|
|
//========= Copyright Valve Corporation, All rights reserved. ============//
//
// Purpose:
//
//===========================================================================//
#include "cbase.h"
#include "isoundcombiner.h"
#include "sentence.h"
#include "filesystem.h"
#include "tier2/riff.h"
#include "tier1/utlbuffer.h"
#include "snd_audio_source.h"
#include "snd_wave_source.h"
#include "AudioWaveOutput.h"
#include "ifaceposersound.h"
#include "vstdlib/random.h"
#include "checksum_crc.h"
#define WAVEOUTPUT_BITSPERCHANNEL 16
#define WAVEOUTPUT_FREQUENCY 44100
class CSoundCombiner : public ISoundCombiner { public: CSoundCombiner() : m_pWaveOutput( NULL ), m_pOutRIFF( NULL ), m_pOutIterator( NULL ) { m_szOutFile[ 0 ] = 0; }
virtual bool CombineSoundFiles( IFileSystem *filesystem, char const *outfile, CUtlVector< CombinerEntry >& info ); virtual bool IsCombinedFileChecksumValid( IFileSystem *filesystem, char const *outfile, CUtlVector< CombinerEntry >& info );
private:
struct CombinerWork { CombinerWork() : sentence(), duration( 0.0 ), wave( 0 ), mixer( 0 ), entry( 0 ) { } CSentence sentence; float duration; CAudioSource *wave; CAudioMixer *mixer; CombinerEntry *entry; };
bool InternalCombineSoundFiles( IFileSystem *filesystem, char const *outfile, CUtlVector< CombinerEntry >& info ); bool VerifyFilesExist( IFileSystem *filesystem, CUtlVector< CombinerEntry >& info ); bool CreateWorkList( IFileSystem *filesystem, CUtlVector< CombinerEntry >& info );
bool PerformSplicingOnWorkItems( IFileSystem *filesystem ); void CleanupWork();
// .wav file utils
int ComputeBestNumChannels(); void ParseSentence( CSentence& sentence, IterateRIFF &walk ); bool LoadSentenceFromWavFileUsingIO( char const *wavfile, CSentence& sentence, IFileReadBinary& io ); bool LoadSentenceFromWavFile( char const *wavfile, CSentence& sentence ); void StoreValveDataChunk( CSentence& sentence ); // bool SaveSentenceToWavFile( char const *wavfile, CSentence& sentence );
bool InitSplicer( IFileSystem *filesystem, int samplerate, int numchannels, int bitspersample ); bool LoadSpliceAudioSources(); bool AppendSilence( int ¤tsample, float duration ); bool AppendStereo16Data( short samples[ 2 ] ); bool AppendWaveData( int& currentsample, CAudioSource *wave, CAudioMixer *mixer ); void AddSentenceToCombined( float offset, CSentence& sentence );
unsigned int CheckSumWork( IFileSystem *filesystem, CUtlVector< CombinerEntry >& info ); unsigned int ComputeChecksum();
CUtlVector< CombinerWork * > m_Work; CSentence m_Combined;
CAudioWaveOutput *m_pWaveOutput;
OutFileRIFF *m_pOutRIFF; IterateOutputRIFF *m_pOutIterator;
int m_nSampleRate; int m_nNumChannels; int m_nBitsPerSample; int m_nBytesPerSample; char m_szOutFile[ MAX_PATH ]; };
static CSoundCombiner g_SoundCombiner; ISoundCombiner *soundcombiner = &g_SoundCombiner;
bool CSoundCombiner::CreateWorkList( IFileSystem *pFilesystem, CUtlVector< CombinerEntry >& info ) { m_Work.RemoveAll();
int c = info.Count(); for ( int i = 0; i < c; ++i ) { CombinerWork *workitem = new CombinerWork();
char fullpath[ MAX_PATH ]; Q_strncpy( fullpath, info[ i ].wavefile, sizeof( fullpath ) ); pFilesystem->GetLocalPath( info[ i ].wavefile, fullpath, sizeof( fullpath ) );
if ( !LoadSentenceFromWavFile( fullpath, workitem->sentence ) ) { Warning( "CSoundCombiner::CreateWorkList couldn't load %s for work item (%d)\n", fullpath, i ); return false; } workitem->entry = &info[ i ];
m_Work.AddToTail( workitem ); }
return true; }
void CSoundCombiner::CleanupWork() { int c = m_Work.Count(); for ( int i = 0; i < c; ++i ) { CombinerWork *workitem = m_Work[ i ]; delete workitem->mixer; delete workitem->wave;
delete m_Work[ i ]; } m_Work.RemoveAll();
delete m_pOutIterator; m_pOutIterator = NULL;
delete m_pOutRIFF; m_pOutRIFF = NULL; }
bool CSoundCombiner::InternalCombineSoundFiles( IFileSystem *pFilesystem, char const *outfile, CUtlVector< CombinerEntry >& info ) { Q_strncpy( m_szOutFile, outfile, sizeof( m_szOutFile ) ); if ( info.Count() <= 0 ) { Warning( "CSoundCombiner::InternalCombineSoundFiles: work item count is zero\n" ); return false; }
if ( !VerifyFilesExist( pFilesystem, info ) ) { return false; }
if ( !CreateWorkList( pFilesystem, info ) ) { return false; }
PerformSplicingOnWorkItems( pFilesystem );
return true; }
bool CSoundCombiner::CombineSoundFiles( IFileSystem *pFilesystem, char const *outfile, CUtlVector< CombinerEntry >& info ) { bool bret = InternalCombineSoundFiles( pFilesystem, outfile, info ); CleanupWork(); return bret; }
unsigned int CSoundCombiner::ComputeChecksum() { CRC32_t crc; CRC32_Init( &crc ); int c = m_Work.Count(); for ( int i = 0; i < c; ++i ) { CombinerWork *curitem = m_Work[ i ]; unsigned int chk = curitem->sentence.ComputeDataCheckSum();
// Msg( " %i -> sentence %u, startoffset %f fn %s\n",
// i, chk, curitem->entry->startoffset, curitem->entry->wavefile );
CRC32_ProcessBuffer( &crc, &chk, sizeof( unsigned long ) ); CRC32_ProcessBuffer( &crc, &curitem->entry->startoffset, sizeof( float ) ); CRC32_ProcessBuffer( &crc, curitem->entry->wavefile, Q_strlen( curitem->entry->wavefile ) ); }
CRC32_Final( &crc ); return ( unsigned int )crc; }
unsigned int CSoundCombiner::CheckSumWork( IFileSystem *pFilesystem, CUtlVector< CombinerEntry >& info ) { if ( info.Count() <= 0 ) { Warning( "CSoundCombiner::CheckSumWork: work item count is zero\n" ); return 0; }
if ( !VerifyFilesExist( pFilesystem, info ) ) { return 0; }
if ( !CreateWorkList( pFilesystem, info ) ) { return 0; }
// Checkum work items
unsigned int checksum = ComputeChecksum();
return checksum; }
bool CSoundCombiner::IsCombinedFileChecksumValid( IFileSystem *pFilesystem, char const *outfile, CUtlVector< CombinerEntry >& info ) { unsigned int computedChecksum = CheckSumWork( pFilesystem, info );
char fullpath[ MAX_PATH ]; Q_strncpy( fullpath, outfile, sizeof( fullpath ) ); pFilesystem->GetLocalPath( outfile, fullpath, sizeof( fullpath ) );
CSentence sentence;
bool valid = false;
if ( LoadSentenceFromWavFile( fullpath, sentence ) ) { unsigned int diskFileEmbeddedChecksum = sentence.GetDataCheckSum();
valid = computedChecksum == diskFileEmbeddedChecksum;
if ( !valid ) { Warning( " checksum computed %u, disk %u\n", computedChecksum, diskFileEmbeddedChecksum ); } } else { Warning( "CSoundCombiner::IsCombinedFileChecksumValid: Unabled to load %s\n", fullpath ); }
CleanupWork(); return valid; }
bool CSoundCombiner::VerifyFilesExist( IFileSystem *pFilesystem, CUtlVector< CombinerEntry >& info ) { int c = info.Count(); for ( int i = 0 ; i < c; ++i ) { CombinerEntry& entry = info[ i ]; if ( !pFilesystem->FileExists( entry.wavefile ) ) { Warning( "CSoundCombiner::VerifyFilesExist: missing file %s\n", entry.wavefile ); return false; } } return true; }
//-----------------------------------------------------------------------------
// Purpose: Implements the RIFF i/o interface on stdio
//-----------------------------------------------------------------------------
class StdIOReadBinary : public IFileReadBinary { public: int open( const char *pFileName ) { return (int)filesystem->Open( pFileName, "rb" ); }
int read( void *pOutput, int size, int file ) { if ( !file ) return 0;
return filesystem->Read( pOutput, size, (FileHandle_t)file ); }
void seek( int file, int pos ) { if ( !file ) return;
filesystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD ); }
unsigned int tell( int file ) { if ( !file ) return 0;
return filesystem->Tell( (FileHandle_t)file ); }
unsigned int size( int file ) { if ( !file ) return 0;
return filesystem->Size( (FileHandle_t)file ); }
void close( int file ) { if ( !file ) return;
filesystem->Close( (FileHandle_t)file ); } };
class StdIOWriteBinary : public IFileWriteBinary { public: int create( const char *pFileName ) { return (int)filesystem->Open( pFileName, "wb" ); }
int write( void *pData, int size, int file ) { return filesystem->Write( pData, size, (FileHandle_t)file ); }
void close( int file ) { filesystem->Close( (FileHandle_t)file ); }
void seek( int file, int pos ) { filesystem->Seek( (FileHandle_t)file, pos, FILESYSTEM_SEEK_HEAD ); }
unsigned int tell( int file ) { return filesystem->Tell( (FileHandle_t)file ); } };
static StdIOReadBinary io_in; static StdIOWriteBinary io_out;
#define RIFF_WAVE MAKEID('W','A','V','E')
#define WAVE_FMT MAKEID('f','m','t',' ')
#define WAVE_DATA MAKEID('d','a','t','a')
#define WAVE_FACT MAKEID('f','a','c','t')
#define WAVE_CUE MAKEID('c','u','e',' ')
//-----------------------------------------------------------------------------
// Purpose:
// Input : &walk -
//-----------------------------------------------------------------------------
void CSoundCombiner::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 CSoundCombiner::LoadSentenceFromWavFileUsingIO( char const *wavfile, CSentence& sentence, IFileReadBinary& io ) { sentence.Reset();
InFileRIFF riff( wavfile, io );
// UNDONE: Don't use printf to handle errors
if ( riff.RIFFName() != RIFF_WAVE ) { return false; }
// set up the iterator for the whole file (root RIFF is a chunk)
IterateRIFF walk( riff, riff.RIFFSize() );
// This chunk must be first as it contains the wave's format
// break out when we've parsed it
bool found = false; while ( walk.ChunkAvailable() && !found ) { switch( walk.ChunkName() ) { case WAVE_VALVEDATA: { found = true; CSoundCombiner::ParseSentence( sentence, walk ); } break; } walk.ChunkNext(); }
return true; }
bool CSoundCombiner::LoadSentenceFromWavFile( char const *wavfile, CSentence& sentence ) { return CSoundCombiner::LoadSentenceFromWavFileUsingIO( wavfile, sentence, io_in ); }
//-----------------------------------------------------------------------------
// Purpose:
// Input : store -
//-----------------------------------------------------------------------------
void CSoundCombiner::StoreValveDataChunk( CSentence& sentence ) { // Buffer and dump data
CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER );
sentence.SaveToBuffer( buf );
// Copy into store
m_pOutIterator->ChunkWriteData( buf.Base(), buf.TellPut() ); }
/*
bool CSoundCombiner::SaveSentenceToWavFile( char const *wavfile, CSentence& sentence ) { char tempfile[ 512 ];
Q_StripExtension( wavfile, tempfile, sizeof( tempfile ) ); Q_DefaultExtension( tempfile, ".tmp", sizeof( tempfile ) );
if ( filesystem->FileExists( tempfile, NULL ) ) { filesystem->RemoveFile( tempfile, NULL ); }
if ( !filesystem->IsFileWritable( wavfile ) ) { Msg( "%s is not writable, can't save sentence data to file\n", wavfile ); return false; } // Rename original wavfile to temp
filesystem->RenameFile( wavfile, tempfile, NULL );
// NOTE: Put this in it's own scope so that the destructor for outfileRFF actually closes the file!!!!
{ // Read from Temp
InFileRIFF riff( tempfile, io_in ); 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 wavfile by name
OutFileRIFF riffout( wavfile, io_out );
IterateOutputRIFF store( riffout );
bool wordtrackwritten = false;
// Walk input chunks and copy to output
while ( walk.ChunkAvailable() ) { m_pOutIterator->ChunkStart( walk.ChunkName() );
switch ( walk.ChunkName() ) { case WAVE_VALVEDATA: { // Overwrite data
CSoundCombiner::StoreValveDataChunk( sentence ); wordtrackwritten = true; } break; default: m_pOutIterator->CopyChunkData( walk ); break; }
m_pOutIterator->ChunkFinish();
walk.ChunkNext(); }
// If we didn't write it above, write it now
if ( !wordtrackwritten ) { m_pOutIterator->ChunkStart( WAVE_VALVEDATA ); CSoundCombiner::StoreValveDataChunk( sentence ); m_pOutIterator->ChunkFinish(); } }
// Remove temp file
filesystem->RemoveFile( tempfile, NULL );
return true; } */
typedef struct channel_s { int leftvol; int rightvol; int rleftvol; int rrightvol; float pitch; } channel_t;
bool CSoundCombiner::InitSplicer( IFileSystem *pFilesystem, int samplerate, int numchannels, int bitspersample ) { m_nSampleRate = samplerate; m_nNumChannels = numchannels; m_nBitsPerSample = bitspersample; m_nBytesPerSample = bitspersample >> 3;
m_pWaveOutput = ( CAudioWaveOutput * )sound->GetAudioOutput(); if ( !m_pWaveOutput ) { Warning( "CSoundCombiner::InitSplicer m_pWaveOutput == NULL\n" ); return false; }
// Make sure the directory exists
char basepath[ 512 ]; Q_ExtractFilePath( m_szOutFile, basepath, sizeof( basepath ) ); pFilesystem->CreateDirHierarchy( basepath, "GAME" );
// Create out put file
m_pOutRIFF = new OutFileRIFF( m_szOutFile, io_out ); if ( !m_pOutRIFF ) { Warning( "CSoundCombiner::InitSplicer m_pOutRIFF == NULL\n" ); return false; }
// Create output iterator
m_pOutIterator = new IterateOutputRIFF( *m_pOutRIFF ); if ( !m_pOutIterator ) { Warning( "CSoundCombiner::InitSplicer m_pOutIterator == NULL\n" ); return false; }
WAVEFORMATEX format; format.cbSize = sizeof( format );
format.wFormatTag = WAVE_FORMAT_PCM; format.nAvgBytesPerSec = m_nSampleRate * m_nNumChannels * m_nBytesPerSample; format.nChannels = m_nNumChannels; format.wBitsPerSample = m_nBitsPerSample; format.nSamplesPerSec = m_nSampleRate; format.nBlockAlign = 1;
// Always store the format chunk first
m_pOutIterator->ChunkWrite( WAVE_FMT, &format, sizeof( format ) );
return true; }
bool CSoundCombiner::LoadSpliceAudioSources() { int c = m_Work.Count(); for ( int i = 0; i < c; ++i ) { CombinerWork *item = m_Work[ i ];
CAudioSource *wave = sound->LoadSound( item->entry->wavefile ); if ( !wave ) { Warning( "CSoundCombiner::LoadSpliceAudioSources LoadSound failed '%s'\n", item->entry->wavefile ); return false; }
CAudioMixer *pMixer = wave->CreateMixer(); if ( !pMixer ) { Warning( "CSoundCombiner::LoadSpliceAudioSources CreateMixer failed '%s'\n", item->entry->wavefile ); return false; } item->wave = wave; item->mixer = pMixer; item->duration = wave->GetRunningLength(); }
return true; }
bool CSoundCombiner::AppendSilence( int ¤tsample, float duration ) { int numSamples = duration * m_nSampleRate;
#define MOTION_RANGE 150
#define MOTION_MAXSTEP 20
int currentValue = 32767; int maxValue = currentValue + ( MOTION_RANGE / 2 ); int minValue = currentValue - ( MOTION_RANGE / 2 );
short samples[ 2 ];
while ( --numSamples >= 0 ) { currentValue += random->RandomInt( -MOTION_MAXSTEP, MOTION_MAXSTEP ); currentValue = min( maxValue, currentValue ); currentValue = max( minValue, currentValue );
// Downsample to 0 65556 range
short s = (float)currentValue / 32768.0f;
samples[ 0 ] = s; samples[ 1 ] = s;
AppendStereo16Data( samples ); }
return true; }
bool CSoundCombiner::AppendStereo16Data( short samples[ 2 ] ) { // Convert from 16 bit, 2 channels to output size
if ( m_nNumChannels == 1 ) { if ( m_nBytesPerSample == 1 ) { // Convert to 8 bit mono
// left + right (2 channels ) * 16 bits
float s1 = (float)( samples[ 0 ] >> 8 ); float s2 = (float)( samples[ 1 ] >> 8 );
float avg = ( s1 + s2 ) * 0.5f; avg = clamp( avg, -127.0f, 127.0f ); byte chopped = (byte)( avg+ 127 );
m_pOutIterator->ChunkWriteData( &chopped, sizeof( byte ) ); } else if ( m_nBytesPerSample == 2 ) { // Conver to 16 bit mono
float s1 = (float)( samples[ 0 ] ); float s2 = (float)( samples[ 1 ] );
float avg = ( s1 + s2 ) * 0.5f; unsigned short chopped = (unsigned short)( avg );
m_pOutIterator->ChunkWriteData( &chopped, sizeof( unsigned short ) ); } else { Assert( 0 ); return false; } } else if ( m_nNumChannels == 2 ) { if ( m_nBytesPerSample == 1 ) { // Convert to 8 bit stereo
// left + right (2 channels ) * 16 bits
float s1 = (float)( samples[ 0 ] >> 8 ); float s2 = (float)( samples[ 1 ] >> 8 );
s1 = clamp( s1, -127.0f, 127.0f ); s2 = clamp( s2, -127.0f, 127.0f );
byte chopped1 = (byte)( s1 + 127.0f ); byte chopped2 = (byte)( s2 + 127.0f );
m_pOutIterator->ChunkWriteData( &chopped1, sizeof( byte ) ); m_pOutIterator->ChunkWriteData( &chopped2, sizeof( byte ) ); } else if ( m_nBytesPerSample == 2 ) { // Leave as 16 bit stereo
// Directly store values
m_pOutIterator->ChunkWriteData( &samples[ 0 ], sizeof( unsigned short ) ); m_pOutIterator->ChunkWriteData( &samples[ 1 ], sizeof( unsigned short ) ); } else { Assert( 0 ); return false; } } else { Assert( 0 ); return false; }
return true; }
bool CSoundCombiner::AppendWaveData( int& currentsample, CAudioSource *wave, CAudioMixer *mixer ) { // need a bit of space
short samples[ 2 ]; channel_t channel; memset( &channel, 0, sizeof( channel ) ); channel.leftvol = 255; channel.rightvol = 255; channel.pitch = 1.0;
while ( 1 ) { m_pWaveOutput->m_audioDevice.MixBegin();
if ( !mixer->MixDataToDevice( &m_pWaveOutput->m_audioDevice, &channel, currentsample, 1, wave->SampleRate(), true ) ) break;
m_pWaveOutput->m_audioDevice.TransferBufferStereo16( samples, 1 );
currentsample = mixer->GetSamplePosition();
AppendStereo16Data( samples ); }
return true; }
int CSoundCombiner::ComputeBestNumChannels() { // We prefer mono output unless one of the source wav files is stereo, then we'll do stereo output
int c = m_Work.Count(); for ( int i = 0; i < c; ++i ) { CombinerWork *curitem = m_Work[ i ];
if ( curitem->wave->GetNumChannels() == 2 ) { return 2; } } return 1; }
bool CSoundCombiner::PerformSplicingOnWorkItems( IFileSystem *pFilesystem ) { if ( !LoadSpliceAudioSources() ) { return false; }
int bestNumChannels = ComputeBestNumChannels(); int bitsPerChannel = WAVEOUTPUT_BITSPERCHANNEL;
// Pull in data and write it out
if ( !InitSplicer( pFilesystem, WAVEOUTPUT_FREQUENCY, bestNumChannels, bitsPerChannel ) ) { return false; }
m_pOutIterator->ChunkStart( WAVE_DATA );
float timeoffset = 0.0f;
m_Combined.Reset(); m_Combined.SetText( "" );
int c = m_Work.Count(); for ( int i = 0; i < c; ++i ) { int currentsample = 0;
CombinerWork *curitem = m_Work[ i ]; CombinerWork *nextitem = NULL; if ( i != c - 1 ) { nextitem = m_Work[ i + 1 ]; }
float duration = curitem->duration;
AppendWaveData( currentsample, curitem->wave, curitem->mixer );
AddSentenceToCombined( timeoffset, curitem->sentence );
timeoffset += duration;
if ( nextitem != NULL ) { float nextstart = nextitem->entry->startoffset; float silence_time = nextstart - timeoffset;
AppendSilence( currentsample, silence_time );
timeoffset += silence_time; } }
m_pOutIterator->ChunkFinish();
// Checksum the work items
unsigned int checksum = ComputeChecksum();
// Make sure the checksum is embedded in the data file
m_Combined.SetDataCheckSum( checksum );
// Msg( " checksum computed %u\n", checksum );
m_pOutIterator->ChunkStart( WAVE_VALVEDATA ); StoreValveDataChunk( m_Combined ); m_pOutIterator->ChunkFinish();
return true; }
void CSoundCombiner::AddSentenceToCombined( float offset, CSentence& sentence ) { m_Combined.Append( offset, sentence ); }
|