source-engine/public/soundcombiner.cpp

824 lines
20 KiB
C++
Raw Permalink Normal View History

2020-04-22 16:56:21 +00:00
//========= 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 &currentsample, 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 &currentsample, 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 );
}