//========= 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 ); }