source-engine/sfmobjects/sfmphonemeextractor.cpp

1187 lines
37 KiB
C++
Raw Permalink Normal View History

2020-04-23 00:56:21 +08:00
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//=============================================================================
#include "sfmobjects/SFMPhonemeExtractor.h"
#include "tier2/riff.h"
#include "PhonemeConverter.h"
#include "filesystem.h"
#include "tier1/utlbuffer.h"
#include "sentence.h"
#include "movieobjects/dmesound.h"
#include "movieobjects/dmeanimationset.h"
#include "movieobjects/dmebookmark.h"
#include "movieobjects/dmeclip.h"
#include "movieobjects/dmechannel.h"
#include "soundchars.h"
#include "tier2/p4helpers.h"
#include "tier2/soundutils.h"
#include "tier1/utldict.h"
#include <windows.h> // WAVEFORMATEX, WAVEFORMAT and ADPCM WAVEFORMAT!!!
#include <mmreg.h>
// memdbgon must be the last include file in a .cpp file!!!
#include "tier0/memdbgon.h"
static const char *s_pAttributeValueNames[LOG_PREVIEW_FLEX_CHANNEL_COUNT] =
{
"value",
"balance",
"multilevel"
};
static const char *s_pDefaultAttributeValueNames[LOG_PREVIEW_FLEX_CHANNEL_COUNT] =
{
"defaultValue",
"defaultBalance",
"defaultMultilevel"
};
struct Extractor
{
PE_APITYPE apitype;
CSysModule *module;
IPhonemeExtractor *extractor;
};
//-----------------------------------------------------------------------------
// Implementations of the phoneme extractor
//-----------------------------------------------------------------------------
class CSFMPhonemeExtractor : public ISFMPhonemeExtractor
{
public:
CSFMPhonemeExtractor();
// Inherited from ISFMPhonemeExtractor
virtual bool Init();
virtual void Shutdown();
virtual int GetAPICount();
virtual void GetAPIInfo( int index, CUtlString* pPrintName, PE_APITYPE *pAPIType );
virtual void Extract( const PE_APITYPE& apiType, ExtractDesc_t& info, bool bWritePhonemesToWavFiles );
virtual void ReApply( ExtractDesc_t& info );
virtual bool GetSentence( CDmeGameSound *gameSound, CSentence& sentence );
private:
int FindExtractor( PE_APITYPE type );
bool GetWaveFormat( const char *filename, CUtlBuffer* pFormat, int *pDataSize, CSentence& sentence, bool &bGotSentence );
void LogPhonemes( int nItemIndex, ExtractDesc_t& info );
void ClearInterstitialSpaces( CDmeChannelsClip *pChannelsClip, CUtlDict< LogPreview_t *, int >& controlLookup, ExtractDesc_t& info );
void StampControlValueLogs( CDmePreset *preset, DmeTime_t tHeadPosition, float flIntensity, CUtlDict< LogPreview_t *, int > &controlLookup );
void WriteCurrentValuesIntoLogLayers( DmeTime_t tHeadPosition, const CUtlDict< LogPreview_t *, int > &controlLookup );
void WriteDefaultValuesIntoLogLayers( DmeTime_t tHeadPosition, const CUtlDict< LogPreview_t *, int > &controlLookup );
void BuildPhonemeLogList( CUtlVector< LogPreview_t > &list, CUtlVector< CDmeLog * > &logs );
CDmeChannelsClip* FindFacialChannelsClip( const CUtlVector< LogPreview_t > &list );
void BuildPhonemeToPresetMapping( const CUtlVector< CBasePhonemeTag * > &stream, CDmeAnimationSet *pSet, CDmePresetGroup * pPresetGroup, CUtlDict< CDmePreset *, unsigned short > &phonemeToPresetDict );
CUtlVector< Extractor > m_Extractors;
int m_nCurrentExtractor;
};
//-----------------------------------------------------------------------------
// Singleton
//-----------------------------------------------------------------------------
static CSFMPhonemeExtractor g_ExtractorSingleton;
ISFMPhonemeExtractor *sfm_phonemeextractor = &g_ExtractorSingleton;
//-----------------------------------------------------------------------------
// Constructor
//-----------------------------------------------------------------------------
CSFMPhonemeExtractor::CSFMPhonemeExtractor() : m_nCurrentExtractor( -1 )
{
}
//-----------------------------------------------------------------------------
// Init, shutdown
//-----------------------------------------------------------------------------
bool CSFMPhonemeExtractor::Init()
{
// Enumerate modules under bin folder of exe
FileFindHandle_t findHandle;
const char *pFilename = g_pFullFileSystem->FindFirstEx( "phonemeextractors/*.dll", "EXECUTABLE_PATH", &findHandle );
while( pFilename )
{
char fullpath[ 512 ];
Q_snprintf( fullpath, sizeof( fullpath ), "phonemeextractors/%s", pFilename );
// Msg( "Loading extractor from %s\n", fullpath );
Extractor e;
e.module = g_pFullFileSystem->LoadModule( fullpath );
if ( !e.module )
{
pFilename = g_pFullFileSystem->FindNext( findHandle );
continue;
}
CreateInterfaceFn factory = Sys_GetFactory( e.module );
if ( !factory )
{
pFilename = g_pFullFileSystem->FindNext( findHandle );
continue;
}
e.extractor = ( IPhonemeExtractor * )factory( VPHONEME_EXTRACTOR_INTERFACE, NULL );
if ( !e.extractor )
{
Warning( "Unable to get IPhonemeExtractor interface version %s from %s\n", VPHONEME_EXTRACTOR_INTERFACE, fullpath );
pFilename = g_pFullFileSystem->FindNext( findHandle );
continue;
}
e.apitype = e.extractor->GetAPIType();
m_Extractors.AddToTail( e );
pFilename = g_pFullFileSystem->FindNext( findHandle );
}
g_pFullFileSystem->FindClose( findHandle );
return true;
}
void CSFMPhonemeExtractor::Shutdown()
{
int c = m_Extractors.Count();
for ( int i = c - 1; i >= 0; i-- )
{
Extractor *e = &m_Extractors[ i ];
g_pFullFileSystem->UnloadModule( e->module );
}
m_Extractors.RemoveAll();
}
//-----------------------------------------------------------------------------
// Finds an extractor of a particular type
//-----------------------------------------------------------------------------
int CSFMPhonemeExtractor::FindExtractor( PE_APITYPE type )
{
for ( int i=0; i < m_Extractors.Count(); i++ )
{
if ( m_Extractors[i].apitype == type )
return i;
}
return -1;
}
//-----------------------------------------------------------------------------
// Iterates over extractors
//-----------------------------------------------------------------------------
int CSFMPhonemeExtractor::GetAPICount()
{
return m_Extractors.Count();
}
void CSFMPhonemeExtractor::GetAPIInfo( int index, CUtlString* pPrintName, PE_APITYPE *pAPIType )
{
Assert( pPrintName );
Assert( pAPIType );
pPrintName->Set( m_Extractors[ index ].extractor->GetName() );
*pAPIType = m_Extractors[ index ].apitype;
}
static void ParseSentence( CSentence& sentence, IterateRIFF &walk )
{
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
buf.EnsureCapacity( walk.ChunkSize() );
walk.ChunkRead( buf.Base() );
buf.SeekPut( CUtlBuffer::SEEK_HEAD, walk.ChunkSize() );
sentence.InitFromDataChunk( buf.Base(), buf.TellPut() );
}
bool CSFMPhonemeExtractor::GetWaveFormat( const char *filename, CUtlBuffer *pBuf, int *pDataSize, CSentence& sentence, bool &bGotSentence )
{
InFileRIFF riff( filename, *g_pFSIOReadBinary );
Assert( riff.RIFFName() == RIFF_WAVE );
// set up the iterator for the whole file (root RIFF is a chunk)
IterateRIFF walk( riff, riff.RIFFSize() );
bool gotFmt = false;
bool gotData = false;
bGotSentence = false;
// Walk input chunks and copy to output
while ( walk.ChunkAvailable() )
{
switch ( walk.ChunkName() )
{
case WAVE_FMT:
{
pBuf->SeekPut( CUtlBuffer::SEEK_HEAD, walk.ChunkSize() );
walk.ChunkRead( pBuf->Base() );
gotFmt = true;
}
break;
case WAVE_DATA:
{
*pDataSize = walk.ChunkSize();
gotData = true;
}
break;
case WAVE_VALVEDATA:
{
bGotSentence = true;
ParseSentence( sentence, walk );
}
break;
default:
break;
}
// Done
if ( gotFmt && gotData && bGotSentence )
return true;
walk.ChunkNext();
}
return ( gotFmt && gotData );
}
bool CSFMPhonemeExtractor::GetSentence( CDmeGameSound *gameSound, CSentence& sentence )
{
const char *filename = gameSound->m_SoundName.Get();
Assert( filename && filename [ 0 ] );
char soundname[ 512 ];
// Note, calling PSkipSoundChars to remove any decorator characters used by the engine!!!
Q_snprintf( soundname, sizeof( soundname ), "sound/%s", PSkipSoundChars( filename ) );
Q_FixSlashes( soundname );
char fullpath[ 512 ];
g_pFullFileSystem->RelativePathToFullPath( soundname, "GAME", fullpath, sizeof( fullpath ) );
// Get sound file metrics of interest
CUtlBuffer buf;
int nDataSize;
bool bValidSentence = false;
if ( !GetWaveFormat( soundname, &buf, &nDataSize, sentence, bValidSentence ) )
return false;
return bValidSentence;
}
static void BuildPhonemeStream( CSentence& in, CUtlVector< CBasePhonemeTag * >& list )
{
for ( int i = 0; i < in.m_Words.Count(); ++i )
{
CWordTag *w = in.m_Words[ i ];
if ( !w )
continue;
for ( int j = 0; j < w->m_Phonemes.Count(); ++j )
{
CPhonemeTag *ph = w->m_Phonemes[ j ];
if ( !ph )
continue;
CBasePhonemeTag *newTag = new CBasePhonemeTag( *ph );
list.AddToTail( newTag );
}
}
if ( !in.m_Words.Count() && in.m_RunTimePhonemes.Count() )
{
for ( int i = 0 ; i < in.m_RunTimePhonemes.Count(); ++i )
{
CBasePhonemeTag *newTag = new CBasePhonemeTag( *in.m_RunTimePhonemes[ i ] );
list.AddToTail( newTag );
}
}
}
//-----------------------------------------------------------------------------
// Purpose: Same the phoneme data into the sound files
//-----------------------------------------------------------------------------
static void StoreValveDataChunk( CSentence& sentence, IterateOutputRIFF& store )
{
// Buffer and dump data
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
sentence.SaveToBuffer( buf );
// Copy into store
store.ChunkWriteData( buf.Base(), buf.TellPut() );
}
static bool SaveSentenceToWavFile( const char *pWavFile, CSentence& sentence )
{
char pTempFile[ 512 ];
Q_StripExtension( pWavFile, pTempFile, sizeof( pTempFile ) );
Q_DefaultExtension( pTempFile, ".tmp", sizeof( pTempFile ) );
if ( g_pFullFileSystem->FileExists( pTempFile, "GAME" ) )
{
g_pFullFileSystem->RemoveFile( pTempFile, "GAME" );
}
CP4AutoEditAddFile p4Checkout( pWavFile );
if ( !g_pFullFileSystem->IsFileWritable( pWavFile ) )
{
Warning( "%s is not writable, can't save sentence data to file\n", pWavFile );
return false;
}
// Rename original pWavFile to temp
g_pFullFileSystem->RenameFile( pWavFile, pTempFile, "GAME" );
// NOTE: Put this in it's own scope so that the destructor for outfileRFF actually closes the file!!!!
{
// Read from Temp
InFileRIFF riff( pTempFile, *g_pFSIOReadBinary );
Assert( riff.RIFFName() == RIFF_WAVE );
// set up the iterator for the whole file (root RIFF is a chunk)
IterateRIFF walk( riff, riff.RIFFSize() );
// And put data back into original pWavFile by name
OutFileRIFF riffout( pWavFile, *g_pFSIOWriteBinary );
IterateOutputRIFF store( riffout );
bool bWordTrackWritten = false;
// Walk input chunks and copy to output
while ( walk.ChunkAvailable() )
{
store.ChunkStart( walk.ChunkName() );
switch ( walk.ChunkName() )
{
case WAVE_VALVEDATA:
{
// Overwrite data
StoreValveDataChunk( sentence, store );
bWordTrackWritten = true;
}
break;
default:
store.CopyChunkData( walk );
break;
}
store.ChunkFinish();
walk.ChunkNext();
}
// If we didn't write it above, write it now
if ( !bWordTrackWritten )
{
store.ChunkStart( WAVE_VALVEDATA );
StoreValveDataChunk( sentence, store );
store.ChunkFinish();
}
}
// Remove temp file
g_pFullFileSystem->RemoveFile( pTempFile, NULL );
return true;
}
//-----------------------------------------------------------------------------
// Main entry point for phoneme extraction
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::Extract( const PE_APITYPE& apiType, ExtractDesc_t& info, bool bWritePhonemesToWavFiles )
{
if ( !info.m_pSet )
return;
int iExtractor = FindExtractor( apiType );
if ( iExtractor == -1 )
return;
Extractor& extractor = m_Extractors[ iExtractor ];
int nWorkItem;
for ( nWorkItem = 0; nWorkItem < info.m_WorkList.Count(); ++nWorkItem )
{
CExtractInfo& workItem = info.m_WorkList[ nWorkItem ];
workItem.m_flDuration = 0.0f;
CSentence in;
CSentence out;
in.SetText( workItem.m_sHintText.String() );
out.SetText( workItem.m_sHintText.String() );
const char *pFileName = workItem.m_pSound->m_SoundName.Get();
Assert( pFileName && pFileName [ 0 ] );
char pSoundName[ 512 ];
// Note, calling PSkipSoundChars to remove any decorator characters used by the engine!!!
Q_snprintf( pSoundName, sizeof( pSoundName ), "sound/%s", PSkipSoundChars( pFileName ) );
Q_FixSlashes( pSoundName );
char pFullPath[ 512 ];
g_pFullFileSystem->RelativePathToFullPath( pSoundName, "GAME", pFullPath, sizeof( pFullPath ) );
// Get sound file metrics of interest
CUtlBuffer buf;
WAVEFORMATEX *format;
int nDataSize;
if ( !GetWaveFormat( pSoundName, &buf, &nDataSize, workItem.m_Sentence, workItem.m_bSentenceValid ) )
continue;
format = ( WAVEFORMATEX * )buf.Base();
if ( !( format->wBitsPerSample > ( 1 << 3 ) ) )
{
// Have to warn and early-out here to avoid crashing with "integer divide by zero" below
Warning( "Cannot extract phonemes from '%s', %u bits per sample.\n", pSoundName, format->wBitsPerSample );
continue;
}
int nBitsPerSample = format->wBitsPerSample;
float flSampleRate = (float)format->nSamplesPerSec;
int nChannels = format->nChannels;
int nSampleCount = nDataSize / ( nBitsPerSample >> 3 );
float flTrueSampleSize = ( nBitsPerSample * nChannels ) >> 3;
if ( format->wFormatTag == WAVE_FORMAT_ADPCM )
{
nBitsPerSample = 16;
flTrueSampleSize = 0.5f;
ADPCMWAVEFORMAT *pFormat = (ADPCMWAVEFORMAT *)buf.Base();
int blockSize = ((pFormat->wSamplesPerBlock - 2) * pFormat->wfx.nChannels ) / 2;
blockSize += 7 * pFormat->wfx.nChannels;
int blockCount = nDataSize / blockSize;
int blockRem = nDataSize % blockSize;
// total samples in complete blocks
nSampleCount = blockCount * pFormat->wSamplesPerBlock;
// add remaining in a short block
if ( blockRem )
{
nSampleCount += pFormat->wSamplesPerBlock - (((blockSize - blockRem) * 2) / nChannels);
}
}
if ( flSampleRate > 0.0f )
{
workItem.m_flDuration = (float)nSampleCount / flSampleRate;
}
in.CreateEventWordDistribution( workItem.m_sHintText.String(), workItem.m_flDuration );
if ( !workItem.m_bUseSentence || !workItem.m_bSentenceValid )
{
extractor.extractor->Extract( pFullPath,
(int)( workItem.m_flDuration * flSampleRate * flTrueSampleSize ),
Msg, in, out );
// Tracker 57389:
// Total hack to fix a bug where the Lipsinc extractor is messing up the # channels on 16 bit stereo waves
if ( apiType == SPEECH_API_LIPSINC && nChannels == 2 && nBitsPerSample == 16 )
{
flTrueSampleSize *= 2.0f;
}
float bytespersecond = flSampleRate * flTrueSampleSize;
int i;
// Now convert byte offsets to times
for ( i = 0; i < out.m_Words.Size(); i++ )
{
CWordTag *tag = out.m_Words[ i ];
Assert( tag );
if ( !tag )
continue;
tag->m_flStartTime = ( float )(tag->m_uiStartByte ) / bytespersecond;
tag->m_flEndTime = ( float )(tag->m_uiEndByte ) / bytespersecond;
for ( int j = 0; j < tag->m_Phonemes.Size(); j++ )
{
CPhonemeTag *ptag = tag->m_Phonemes[ j ];
Assert( ptag );
if ( !ptag )
continue;
ptag->SetStartTime( ( float )(ptag->m_uiStartByte ) / bytespersecond );
ptag->SetEndTime( ( float )(ptag->m_uiEndByte ) / bytespersecond );
}
}
if ( bWritePhonemesToWavFiles )
{
SaveSentenceToWavFile( pFullPath, out );
}
}
else
{
Msg( "Using .wav file phonemes for (%s)\n", pSoundName );
out = workItem.m_Sentence;
}
// Now create channel data
workItem.ClearTags();
BuildPhonemeStream( out, workItem.m_ApplyTags );
}
if ( info.m_bCreateBookmarks )
{
info.m_pSet->GetBookmarks().RemoveAll();
}
for ( nWorkItem = 0; nWorkItem < info.m_WorkList.Count(); ++nWorkItem )
{
LogPhonemes( nWorkItem, info );
}
}
//-----------------------------------------------------------------------------
//
//-----------------------------------------------------------------------------
static bool UniquePhonemeLessFunc( CBasePhonemeTag * const & lhs, CBasePhonemeTag * const & rhs )
{
return lhs->GetPhonemeCode() < rhs->GetPhonemeCode();
}
void CSFMPhonemeExtractor::BuildPhonemeToPresetMapping( const CUtlVector< CBasePhonemeTag * > &stream,
CDmeAnimationSet *pSet, CDmePresetGroup *pPresetGroup, CUtlDict< CDmePreset *, unsigned short > &phonemeToPresetDict )
{
int i;
CUtlRBTree< CBasePhonemeTag * > uniquePhonemes( 0, 0, UniquePhonemeLessFunc );
for ( i = 0; i < stream.Count(); ++i )
{
CBasePhonemeTag *tag = stream[ i ];
if ( uniquePhonemes.Find( tag ) == uniquePhonemes.InvalidIndex() )
{
uniquePhonemes.Insert( tag );
}
}
for ( i = uniquePhonemes.FirstInorder(); i != uniquePhonemes.InvalidIndex(); i = uniquePhonemes.NextInorder( i ) )
{
CBasePhonemeTag *tag = uniquePhonemes[ i ];
// Convert phoneme code to text
char ph[ 32 ];
Q_strncpy( ph, ConvertPhoneme( tag->GetPhonemeCode() ), sizeof( ph ) );
char remappedph[ 32 ];
// By default we search for a preset name p_xxx where xxx is the phoneme string
Q_snprintf( remappedph, sizeof( remappedph ), "p_%s", ph );
// Now find the preset in the animation set converter
CDmePhonemeMapping *mapping = pSet->FindMapping( ph );
if ( mapping )
{
Q_strncpy( remappedph, mapping->GetValueString( "preset" ), sizeof( remappedph ) );
}
// Now look up the preset, if it exists
CDmePreset *preset = pPresetGroup->FindPreset( remappedph );
if ( !preset )
{
Warning( "Animation set '%s' missing phoneme preset for '%s' -> '%s'\n",
pSet->GetName(), ph, remappedph );
continue;
}
// Add to dictionary if it's not already there
if ( phonemeToPresetDict.Find( ph ) == phonemeToPresetDict.InvalidIndex() )
{
phonemeToPresetDict.Insert( ph, preset );
}
}
}
//-----------------------------------------------------------------------------
// Finds the channels clip which refers to facial control values
//-----------------------------------------------------------------------------
CDmeChannelsClip* CSFMPhonemeExtractor::FindFacialChannelsClip( const CUtlVector< LogPreview_t > &list )
{
CDmeChannelsClip *pChannelsClip = NULL;
int i;
for ( i = list.Count() - 1; i >= 0; --i )
{
const LogPreview_t &lp = list[i];
CDmeChannelsClip *check = FindAncestorReferencingElement< CDmeChannelsClip >( (CDmElement *)lp.m_hChannels[ 0 ].Get() );
if ( !pChannelsClip && check )
{
pChannelsClip = check;
}
else
{
if ( pChannelsClip != check )
{
Warning( "Selected controls overlap multiple channels clips!!!\n" );
}
}
}
if ( !pChannelsClip )
{
Warning( "Unable to determine destination channels clip!!!\n" );
}
return pChannelsClip;
}
//-----------------------------------------------------------------------------
// Builds the list of logs which target facial control values
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::BuildPhonemeLogList( CUtlVector< LogPreview_t > &list, CUtlVector< CDmeLog * > &logs )
{
for ( int i = 0; i < list.Count(); ++i )
{
LogPreview_t& p = list[ i ];
for ( int channel = 0; channel < LOG_PREVIEW_FLEX_CHANNEL_COUNT; ++channel )
{
CDmeChannel *ch = p.m_hChannels[ channel ];
if ( !ch )
continue;
CDmeLog *log = p.m_hChannels[ channel ]->GetLog();
if ( !log )
continue;
logs.AddToTail( log );
}
}
}
//-----------------------------------------------------------------------------
// Writes default values into all log layers targetting facial control values
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::WriteDefaultValuesIntoLogLayers( DmeTime_t tHeadPosition, const CUtlDict< LogPreview_t *, int > &controlLookup )
{
// Write a zero into all relevant log layers
for ( int j = controlLookup.First(); j != controlLookup.InvalidIndex(); j = controlLookup.Next( j ) )
{
LogPreview_t* lp = controlLookup[ j ];
CDmElement *pControl = lp->m_hControl;
for ( int chIndex = 0; chIndex < LOG_PREVIEW_FLEX_CHANNEL_COUNT; ++chIndex )
{
CDmeChannel *pChannel = lp->m_hChannels[ chIndex ];
if ( !pChannel )
continue;
// Now get the log for the channel
CDmeFloatLog *pFloatLog = CastElement< CDmeFloatLog >( pChannel->GetLog() );
if ( !pFloatLog )
continue;
CDmeFloatLogLayer *pLayer = pFloatLog->GetLayer( pFloatLog->GetTopmostLayer() );
if ( !pLayer )
continue;
float flDefaultValue = pControl->GetValue< float >( s_pDefaultAttributeValueNames[chIndex] );
pLayer->InsertKey( tHeadPosition, flDefaultValue );
}
}
}
//-----------------------------------------------------------------------------
// Creates a new log key based on the interpolated value at that time
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::WriteCurrentValuesIntoLogLayers( DmeTime_t tHeadPosition, const CUtlDict< LogPreview_t *, int > &controlLookup )
{
// Write a zero into all relevant log layers
for ( int j = controlLookup.First(); j != controlLookup.InvalidIndex(); j = controlLookup.Next( j ) )
{
LogPreview_t* lp = controlLookup[ j ];
for ( int chIndex = 0; chIndex < LOG_PREVIEW_FLEX_CHANNEL_COUNT; ++chIndex )
{
CDmeChannel *pChannel = lp->m_hChannels[ chIndex ];
if ( !pChannel )
continue;
// Now get the log for the channel
CDmeFloatLog *pFloatLog = CastElement< CDmeFloatLog >( pChannel->GetLog() );
if ( !pFloatLog )
continue;
CDmeFloatLogLayer *pLayer = pFloatLog->GetLayer( pFloatLog->GetTopmostLayer() );
if ( !pLayer )
continue;
float flCurrentValue = pLayer->GetValue( tHeadPosition );
pLayer->InsertKey( tHeadPosition, flCurrentValue );
}
}
}
//-----------------------------------------------------------------------------
// Samples extracted phoneme data and stamps that values into control value logs
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::StampControlValueLogs( CDmePreset *preset, DmeTime_t tHeadPosition, float flIntensity, CUtlDict< LogPreview_t *, int > &controlLookup )
{
// Now walk the logs required by the preset
const CDmrElementArray< CDmElement > &controlValues = preset->GetControlValues( );
for ( int j = 0; j < controlValues.Count(); ++j )
{
// This control contains the preset value
CDmElement *presetControl = controlValues[ j ];
if ( !presetControl )
continue;
int visIndex = controlLookup.Find( presetControl->GetName() );
if ( visIndex == controlLookup.InvalidIndex() )
continue;
LogPreview_t* lp = controlLookup[ visIndex ];
for ( int chIndex = 0; chIndex < LOG_PREVIEW_FLEX_CHANNEL_COUNT; ++chIndex )
{
CDmeChannel *ch = lp->m_hChannels[ chIndex ];
if ( !ch )
continue;
// Whereas this control contains the "default" value for the slider (since the presetControl won't have that value)
CDmElement *defaultValueControl = lp->m_hControl.Get();
if ( !defaultValueControl )
continue;
// Now get the log for the channel
CDmeLog *log = ch->GetLog();
if ( !log )
{
Assert( 0 );
continue;
}
CDmeFloatLog *floatLog = CastElement< CDmeFloatLog >( log );
if ( !floatLog )
continue;
CDmeFloatLogLayer *pLayer = floatLog->GetLayer( floatLog->GetTopmostLayer() );
if ( !pLayer )
continue;
float flDefault = defaultValueControl->GetValue< float >( s_pDefaultAttributeValueNames[chIndex] );
float flControlValue = presetControl->GetValue< float >( s_pAttributeValueNames[ chIndex ] );
float flNewValue = flIntensity * ( flControlValue - flDefault );
float flCurrent = pLayer->GetValue( tHeadPosition ) - flDefault;
// Accumulate new value into topmost layer
pLayer->InsertKey( tHeadPosition, flCurrent + flNewValue + flDefault );
}
}
}
void CSFMPhonemeExtractor::ClearInterstitialSpaces( CDmeChannelsClip *pChannelsClip, CUtlDict< LogPreview_t *, int >& controlLookup, ExtractDesc_t& info )
{
Assert( info.m_pShot );
Assert( pChannelsClip );
if ( info.m_WorkList.Count() == 0 )
return;
// This is handled by the main layering code...
if ( info.m_nExtractType == EXTRACT_WIPE_SOUNDS )
return;
// Now walk through all relevant logs
CUtlVector< CDmeLog * > logs;
BuildPhonemeLogList( info.m_ControlList, logs );
DmeTime_t tMinTime( DMETIME_MAXTIME );
DmeTime_t tMaxTime( DMETIME_MINTIME );
int i;
// Walk work items and figure out time bounds
for ( i = 0; i < info.m_WorkList.Count(); ++i )
{
CExtractInfo &item = info.m_WorkList[ i ];
CUtlVector< CDmeHandle< CDmeClip > > srcStack;
CUtlVector< CDmeHandle< CDmeClip > > dstStack;
// Convert original .wav start to animation set channels clip relative time
item.m_pClip->BuildClipStack( &srcStack, info.m_pMovie, info.m_pShot );
// NOTE: Time bounds measured in sound media time goes from 0 -> flWaveDuration
DmeTime_t tSoundMediaStartTime = CDmeClip::FromChildMediaTime( srcStack, DMETIME_ZERO, false );
DmeTime_t tSoundMediaEndTime = CDmeClip::FromChildMediaTime( srcStack, DmeTime_t( item.m_flDuration ), false );
// NOTE: Start and end time are measured in sound media time
DmeTime_t tStartTime = item.m_pClip->GetStartInChildMediaTime();
DmeTime_t tEndTime = item.m_pClip->GetEndInChildMediaTime();
// And convert back down into channels clip relative time
pChannelsClip->BuildClipStack( &dstStack, info.m_pMovie, info.m_pShot );
// Now convert back down to channels clip relative time
DmeTime_t tChannelMediaStartTime = CDmeClip::ToChildMediaTime( dstStack, tSoundMediaStartTime, false );
DmeTime_t tChannelMediaEndTime = CDmeClip::ToChildMediaTime( dstStack, tSoundMediaEndTime, false );
// Find a scale + offset which transforms data in media space of the sound [namely, the phonemes]
// into the media space of the channels [the logs that drive the facial animation]
DmeTime_t tEndDuration = tChannelMediaEndTime - tChannelMediaStartTime;
double flScale = ( item.m_flDuration != 0.0f ) ? tEndDuration.GetSeconds() / item.m_flDuration : 0.0f;
DmeTime_t tOffset = tChannelMediaStartTime;
DmeTime_t tChannelRelativeStartTime( tStartTime * flScale );
tChannelRelativeStartTime += tOffset;
DmeTime_t tChannelRelativeEndTime( tEndTime * flScale );
tChannelRelativeEndTime += tOffset;
if ( tChannelRelativeStartTime < tMinTime )
{
tMinTime = tChannelRelativeStartTime;
}
if ( tChannelRelativeEndTime > tMaxTime )
{
tMaxTime = tChannelRelativeEndTime;
}
}
// Bloat by one quantum
tMinTime -= DMETIME_MINDELTA;
tMaxTime += DMETIME_MINDELTA;
for ( i = 0; i < logs.Count(); ++i )
{
CDmeLog *log = logs[ i ];
Assert( log->GetNumLayers() == 1 );
CDmeLogLayer *layer = log->GetLayer( log->GetTopmostLayer() );
if ( info.m_nExtractType == EXTRACT_WIPE_RANGE )
{
// Write default value keys into log
// Write a default value at that time
WriteDefaultValuesIntoLogLayers( tMinTime, controlLookup );
// Write a default value at that time
WriteDefaultValuesIntoLogLayers( tMaxTime, controlLookup );
// Now discard all keys > tMinTime and < tMaxTime
for ( int j = layer->GetKeyCount() - 1; j >= 0; --j )
{
DmeTime_t &t = layer->GetKeyTime( j );
if ( t <= tMinTime )
continue;
if ( t >= tMaxTime )
continue;
layer->RemoveKey( j );
}
}
else
{
Assert( info.m_nExtractType == EXTRACT_WIPE_CLIP );
layer->ClearKeys();
}
}
}
void AddAnimSetBookmarkAtSoundMediaTime( const char *pName, DmeTime_t tStart, DmeTime_t tEnd, const CUtlVector< CDmeHandle< CDmeClip > > &srcStack, ExtractDesc_t& info )
{
tStart = CDmeClip::FromChildMediaTime( srcStack, tStart, false );
tEnd = CDmeClip::FromChildMediaTime( srcStack, tEnd, false );
tStart = info.m_pShot->ToChildMediaTime( tStart, false );
tEnd = info.m_pShot->ToChildMediaTime( tEnd, false );
CDmeBookmark *pBookmark = CreateElement< CDmeBookmark >( pName );
pBookmark->SetNote( pName );
pBookmark->SetTime( tStart );
pBookmark->SetDuration( tEnd - tStart );
info.m_pSet->GetBookmarks().AddToTail( pBookmark );
}
//-----------------------------------------------------------------------------
// Main entry point for generating phoneme logs
//-----------------------------------------------------------------------------
void CSFMPhonemeExtractor::LogPhonemes( int nItemIndex, ExtractDesc_t& info )
{
CExtractInfo &item = info.m_WorkList[ nItemIndex ];
// Validate input parameters
Assert( info.m_pSet && item.m_pClip && item.m_pSound );
if ( !info.m_pSet || !item.m_pClip || !item.m_pSound )
return;
CDmePresetGroup *pPresetGroup = info.m_pSet->FindPresetGroup( "phoneme" );
if ( !pPresetGroup )
{
Warning( "Animation set '%s' missing preset group 'phoneme'\n", info.m_pSet->GetName() );
return;
}
if ( !info.m_pSet->GetPhonemeMap().Count() )
{
info.m_pSet->RestoreDefaultPhonemeMap();
}
// Walk through phoneme stack and build list of unique presets
CUtlDict< CDmePreset *, unsigned short > phonemeToPresetDict;
BuildPhonemeToPresetMapping( item.m_ApplyTags, info.m_pSet, pPresetGroup, phonemeToPresetDict );
CDmeChannelsClip *pChannelsClip = FindFacialChannelsClip( info.m_ControlList );
if ( !pChannelsClip )
return;
// Build a fast lookup of the visible sliders
int i;
CUtlDict< LogPreview_t *, int > controlLookup;
for ( i = 0; i < info.m_ControlList.Count(); ++i )
{
controlLookup.Insert( info.m_ControlList[ i ].m_hControl->GetName(), &info.m_ControlList[ i ] );
}
// Only need to do this on the first item and we have multiple .wavs selected
if ( nItemIndex == 0 && info.m_WorkList.Count() > 1 )
{
ClearInterstitialSpaces( pChannelsClip, controlLookup, info );
}
// Set up time selection, put channels into record and stamp out keyframes
// Convert original .wav start to animation set channels clip relative time
CUtlVector< CDmeHandle< CDmeClip > > srcStack;
item.m_pClip->BuildClipStack( &srcStack, info.m_pMovie, info.m_pShot );
if ( srcStack.Count() == 0 )
{
item.m_pClip->BuildClipStack( &srcStack, info.m_pMovie, NULL );
if ( srcStack.Count() == 0 )
{
Msg( "Couldn't build stack sound clip to current shot\n" );
return;
}
}
// NOTE: Time bounds measured in sound media time goes from 0 -> flWaveDuration
DmeTime_t tSoundMediaStartTime = CDmeClip::FromChildMediaTime( srcStack, DMETIME_ZERO, false );
DmeTime_t tSoundMediaEndTime = CDmeClip::FromChildMediaTime( srcStack, DmeTime_t( item.m_flDuration ), false );
// NOTE: Start and end time are measured in sound media time
DmeTime_t tStartTime = item.m_pClip->GetStartInChildMediaTime();
DmeTime_t tEndTime = item.m_pClip->GetEndInChildMediaTime();
// And convert back down into channels clip relative time
CUtlVector< CDmeHandle< CDmeClip > > dstStack;
pChannelsClip->BuildClipStack( &dstStack, info.m_pMovie, info.m_pShot );
// Now convert back down to channels clip relative time
DmeTime_t tChannelMediaStartTime = CDmeClip::ToChildMediaTime( dstStack, tSoundMediaStartTime, false );
DmeTime_t tChannelMediaEndTime = CDmeClip::ToChildMediaTime( dstStack, tSoundMediaEndTime, false );
// Find a scale + offset which transforms data in media space of the sound [namely, the phonemes]
// into the media space of the channels [the logs that drive the facial animation]
DmeTime_t tEndDuration = tChannelMediaEndTime - tChannelMediaStartTime;
double flScale = ( item.m_flDuration != 0.0f ) ? tEndDuration.GetSeconds() / item.m_flDuration : 0.0f;
DmeTime_t tOffset = tChannelMediaStartTime;
CUtlVector< CDmeLog * > logs;
BuildPhonemeLogList( info.m_ControlList, logs );
// Add new write layer to each recording log
for ( i = 0; i < logs.Count(); ++i )
{
logs[ i ]->AddNewLayer();
}
// Iterate over the entire range of the sound
double flStartSoundTime = max( 0, tStartTime.GetSeconds() );
double flEndSoundTime = min( item.m_flDuration, tEndTime.GetSeconds() );
// Stamp keys right before and after the sound so as to
// not generate new values outside the import time range
DmeTime_t tPrePhonemeTime( flStartSoundTime * flScale );
tPrePhonemeTime += tOffset - DMETIME_MINDELTA;
WriteCurrentValuesIntoLogLayers( tPrePhonemeTime, controlLookup );
DmeTime_t tPostPhonemeTime( flEndSoundTime * flScale );
tPostPhonemeTime += tOffset + DMETIME_MINDELTA;
WriteCurrentValuesIntoLogLayers( tPostPhonemeTime, controlLookup );
// add bookmarks
if ( info.m_bCreateBookmarks )
{
AddAnimSetBookmarkAtSoundMediaTime( "start", tPrePhonemeTime, tPrePhonemeTime, srcStack, info );
for ( i = 0; i < item.m_ApplyTags.Count() ; ++i )
{
CBasePhonemeTag *p = item.m_ApplyTags[ i ];
const char *pPhonemeName = ConvertPhoneme( p->GetPhonemeCode() );
DmeTime_t tStart = DmeTime_t( p->GetStartTime() );
DmeTime_t tEnd = DmeTime_t( p->GetEndTime() );
AddAnimSetBookmarkAtSoundMediaTime( pPhonemeName, tStart, tEnd, srcStack, info );
}
AddAnimSetBookmarkAtSoundMediaTime( "end", tPostPhonemeTime, tPostPhonemeTime, srcStack, info );
}
if ( info.m_nFilterType == EXTRACT_FILTER_HOLD || info.m_nFilterType == EXTRACT_FILTER_LINEAR )
{
CDmePreset *pLastPreset = NULL;
for ( i = 0; i < item.m_ApplyTags.Count() ; ++i )
{
CBasePhonemeTag *p = item.m_ApplyTags[ i ];
DmeTime_t tStart = DmeTime_t( p->GetStartTime() );
DmeTime_t tEnd = DmeTime_t( p->GetEndTime() );
int idx = phonemeToPresetDict.Find( ConvertPhoneme( p->GetPhonemeCode() ) );
if ( idx == phonemeToPresetDict.InvalidIndex() )
continue;
CDmePreset *preset = phonemeToPresetDict[ idx ];
if ( !preset )
continue;
DmeTime_t tKeyTime = tStart * flScale + tOffset;
if ( info.m_nFilterType == EXTRACT_FILTER_HOLD )
{
// stamp value at end of phoneme (or default prior to first phoneme)
// NOTE - this ignores phoneme length, but since all phonemes directly abut one another, this doesn't matter
DmeTime_t tLastEnd = tKeyTime - DMETIME_MINDELTA;
if ( tLastEnd > tPrePhonemeTime )
{
WriteDefaultValuesIntoLogLayers( tKeyTime - DMETIME_MINDELTA, controlLookup );
if ( pLastPreset )
{
StampControlValueLogs( pLastPreset, tKeyTime - DMETIME_MINDELTA, 1.0f, controlLookup );
}
}
pLastPreset = preset;
}
WriteDefaultValuesIntoLogLayers( tKeyTime, controlLookup );
StampControlValueLogs( preset, tKeyTime, 1.0f, controlLookup );
if ( info.m_nFilterType == EXTRACT_FILTER_HOLD && i == item.m_ApplyTags.Count() - 1 )
{
// stamp value at end of last phoneme
tKeyTime = tEnd * flScale + tOffset;
tKeyTime = min( tKeyTime, tPostPhonemeTime );
WriteDefaultValuesIntoLogLayers( tKeyTime - DMETIME_MINDELTA, controlLookup );
StampControlValueLogs( preset, tKeyTime - DMETIME_MINDELTA, 1.0f, controlLookup );
// stamp default just after end of last phoneme to hold silence until tPostPhonemeTime
WriteDefaultValuesIntoLogLayers( tKeyTime, controlLookup );
}
}
}
else
{
Assert( info.m_nFilterType == EXTRACT_FILTER_FIXED_WIDTH );
double tStep = 1.0 / (double)clamp( info.m_flSampleRateHz, 1.0f, 1000.0f );
float flFilter = max( info.m_flSampleFilterSize, 0.001f );
float flOOFilter = 1.0f / flFilter;
for ( double t = flStartSoundTime; t < flEndSoundTime; t += tStep )
{
DmeTime_t tPhonemeTime( t );
// Determine the location of the sample in the channels clip
DmeTime_t tKeyTime( t * flScale );
tKeyTime += tOffset;
// Write a default value at that time
WriteDefaultValuesIntoLogLayers( tKeyTime, controlLookup );
// Walk phonemes...
for ( i = 0; i < item.m_ApplyTags.Count() ; ++i )
{
CBasePhonemeTag *p = item.m_ApplyTags[ i ];
DmeTime_t tStart = DmeTime_t( p->GetStartTime() );
DmeTime_t tEnd = DmeTime_t( p->GetEndTime() );
bool bContinue = false;
float flI = 0.0f;
{
DmeTime_t tFilter( flFilter );
if ( tStart >= tPhonemeTime + tFilter || tEnd <= tPhonemeTime )
bContinue = true;
tStart = max( tStart, tPhonemeTime );
tEnd = min( tEnd, tPhonemeTime + tFilter );
flI = ( tEnd - tStart ).GetSeconds() * flOOFilter;
}
DmeTime_t dStart = tStart - tPhonemeTime;
DmeTime_t dEnd = tEnd - tPhonemeTime;
float t1 = dStart.GetSeconds() * flOOFilter;
float t2 = dEnd.GetSeconds() * flOOFilter;
Assert( bContinue == !( t1 < 1.0f && t2 > 0.0f ) );
if ( !( t1 < 1.0f && t2 > 0.0f ) )
continue;
if ( t2 > 1 )
{
t2 = 1;
}
if ( t1 < 0 )
{
t1 = 0;
}
float flIntensity = ( t2 - t1 );
Assert( fabs( flI - flIntensity ) < 0.000001f );
int idx = phonemeToPresetDict.Find( ConvertPhoneme( p->GetPhonemeCode() ) );
if ( idx == phonemeToPresetDict.InvalidIndex() )
continue;
CDmePreset *preset = phonemeToPresetDict[ idx ];
if ( !preset )
continue;
StampControlValueLogs( preset, tKeyTime, flIntensity, controlLookup );
}
}
}
// Flatten write layers
for ( i = 0; i < logs.Count(); ++i )
{
logs[ i ]->FlattenLayers( DMELOG_DEFAULT_THRESHHOLD, CDmeLog::FLATTEN_NODISCONTINUITY_FIXUP );
}
}
void CSFMPhonemeExtractor::ReApply( ExtractDesc_t& info )
{
if ( info.m_bCreateBookmarks )
{
info.m_pSet->GetBookmarks().RemoveAll();
}
for ( int nWorkItem = 0; nWorkItem < info.m_WorkList.Count(); ++nWorkItem )
{
LogPhonemes( nWorkItem, info );
}
}