diff options
Diffstat (limited to 'public/soundcombiner.cpp')
| -rw-r--r-- | public/soundcombiner.cpp | 823 |
1 files changed, 823 insertions, 0 deletions
diff --git a/public/soundcombiner.cpp b/public/soundcombiner.cpp new file mode 100644 index 0000000..dc73c79 --- /dev/null +++ b/public/soundcombiner.cpp @@ -0,0 +1,823 @@ +//========= 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 ); +} |