diff options
Diffstat (limited to 'engine/audio')
83 files changed, 49737 insertions, 0 deletions
diff --git a/engine/audio/audio_pch.cpp b/engine/audio/audio_pch.cpp new file mode 100644 index 0000000..ca79d1f --- /dev/null +++ b/engine/audio/audio_pch.cpp @@ -0,0 +1,10 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +#include "audio_pch.h" diff --git a/engine/audio/audio_pch.h b/engine/audio/audio_pch.h new file mode 100644 index 0000000..177e36a --- /dev/null +++ b/engine/audio/audio_pch.h @@ -0,0 +1,66 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//===========================================================================// + +#include "platform.h" + +#if !defined( _X360 ) && defined( WIN32 ) +#define WIN32_LEAN_AND_MEAN +#pragma warning(push, 1) +#pragma warning(disable: 4005) +#include <windows.h> +#include <mmsystem.h> +#pragma warning(pop) +#include <mmreg.h> +#endif + +#include "basetypes.h" +#include "commonmacros.h" +#include "mathlib/mathlib.h" +#include "tier0/dbg.h" +#include "tier0/vprof.h" +#include "tier0/icommandline.h" +#include "tier1/strtools.h" + +#include "tier2/riff.h" +#include "sound.h" +#include "Color.h" +#include "convar.h" +#include "soundservice.h" +#include "voice_sound_engine_interface.h" +#include "soundflags.h" +#include "filesystem.h" +#include "../filesystem_engine.h" + +#include "snd_device.h" +#include "sound_private.h" +#include "snd_mix_buf.h" +#include "snd_env_fx.h" +#include "snd_channels.h" +#include "snd_audio_source.h" +#include "snd_convars.h" +#include "snd_dev_common.h" +#include "snd_dev_direct.h" +#include "snd_dev_wave.h" +#include "snd_dev_xaudio.h" +#include "snd_sfx.h" +#include "snd_audio_source.h" +#include "snd_wave_source.h" +#include "snd_wave_temp.h" +#include "snd_wave_data.h" +#include "snd_wave_mixer_private.h" +#include "snd_wave_mixer_adpcm.h" +#include "snd_io.h" + +#include "snd_wave_mixer_xma.h" +#if defined( _X360 ) +#include "xbox/xbox_win32stubs.h" +#include <xhv2.h> +#elif POSIX +#include "audio/private/posix_stubs.h" +#endif diff --git a/engine/audio/private/MPAFile.cpp b/engine/audio/private/MPAFile.cpp new file mode 100644 index 0000000..8f9a1b8 --- /dev/null +++ b/engine/audio/private/MPAFile.cpp @@ -0,0 +1,414 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#if defined( WIN32) && !defined( _X360 ) +#include "winlite.h" +#endif +#include "tier0/platform.h" +#include "MPAFile.h" +#include "soundchars.h" +#include "tier1/utlrbtree.h" + +#include "memdbgon.h" + +extern IFileSystem *g_pFullFileSystem; + + +// exception class +CMPAException::CMPAException(ErrorIDs ErrorID, const char *szFile, const char *szFunction, bool bGetLastError ) : +m_ErrorID( ErrorID ), m_bGetLastError( bGetLastError ) +{ + m_szFile = szFile ? strdup(szFile) : NULL; + m_szFunction = szFunction ? strdup(szFunction) : NULL; +} + +// copy constructor (necessary for exception throwing without pointers) +CMPAException::CMPAException(const CMPAException& Source) +{ + m_ErrorID = Source.m_ErrorID; + m_bGetLastError = Source.m_bGetLastError; + m_szFile = Source.m_szFile ? strdup(Source.m_szFile) : NULL; + m_szFunction = Source.m_szFunction ? strdup(Source.m_szFunction) : NULL; +} + +// destructor +CMPAException::~CMPAException() +{ + if( m_szFile ) + free( (void*)m_szFile ); + if( m_szFunction ) + free( (void*)m_szFunction ); +} + +// should be in resource file for multi language applications +const char *m_szErrors[] = +{ + "Can't open the file.", + "Can't set file position.", + "Can't read from file.", + "Reached end of buffer.", + "No VBR Header found.", + "Incomplete VBR Header.", + "No subsequent frame found within tolerance range.", + "No frame found." + +}; + +#define MAX_ERR_LENGTH 256 +void CMPAException::ShowError() +{ + char szErrorMsg[MAX_ERR_LENGTH] = {0}; + char szHelp[MAX_ERR_LENGTH]; + + // this is not buffer-overflow-proof! + if( m_szFunction ) + { + sprintf( szHelp, _T("%s: "), m_szFunction ); + strcat( szErrorMsg, szHelp ); + } + if( m_szFile ) + { + sprintf( szHelp, _T("'%s'\n"), m_szFile ); + strcat( szErrorMsg, szHelp ); + } + strcat( szErrorMsg, m_szErrors[m_ErrorID] ); + +#if defined(WIN32) && !defined(_X360) + if( m_bGetLastError ) + { + // get error message of last system error id + LPVOID pMsgBuf; + if ( FormatMessage( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language + (LPTSTR) &pMsgBuf, + 0, + NULL )) + { + strcat( szErrorMsg, "\n" ); + strcat( szErrorMsg, (const char *)pMsgBuf ); + LocalFree( pMsgBuf ); + } + } +#endif + // show error message + Warning( "%s\n", szErrorMsg ); +} + +// 1KB is inital buffersize, each time the buffer needs to be increased it is doubled +const uint32 CMPAFile::m_dwInitBufferSize = 1024; + + +CMPAFile::CMPAFile( const char * szFile, uint32 dwFileOffset, FileHandle_t hFile ) : +m_pBuffer(NULL), m_dwBufferSize(0), m_dwBegin( dwFileOffset ), m_dwEnd(0), +m_dwNumTimesRead(0), m_bVBRFile( false ), m_pVBRHeader(NULL), m_bMustReleaseFile( false ), +m_pMPAHeader(NULL), m_hFile( hFile ), m_szFile(NULL), m_dwFrameNo(1) +{ + // open file, if not already done + if( m_hFile == FILESYSTEM_INVALID_HANDLE ) + { + Open( szFile ); + m_bMustReleaseFile = true; + } + // save filename + m_szFile = strdup( szFile ); + + // set end of MPEG data (assume file end) + if( m_dwEnd <= 0 ) + { + // get file size + m_dwEnd = g_pFullFileSystem->Size( m_hFile ); + } + + // find first valid MPEG frame + m_pMPAHeader = new CMPAHeader( this ); + + // is VBR header available? + CVBRHeader::VBRHeaderType HeaderType = CVBRHeader::NoHeader; + uint32 dwOffset = m_pMPAHeader->m_dwSyncOffset; + if( CVBRHeader::IsVBRHeaderAvailable( this, HeaderType, dwOffset ) ) + { + try + { + // read out VBR header + m_pVBRHeader = new CVBRHeader( this, HeaderType, dwOffset ); + + m_bVBRFile = true; + m_dwBytesPerSec = m_pVBRHeader->m_dwBytesPerSec; + if( m_pVBRHeader->m_dwBytes > 0 ) + m_dwEnd = m_dwBegin + m_pVBRHeader->m_dwBytes; + } + + catch(CMPAException& Exc) + { + Exc.ShowError(); + } + } + + if( !m_pVBRHeader ) + { + // always skip empty (32kBit) frames + m_bVBRFile = m_pMPAHeader->SkipEmptyFrames(); + m_dwBytesPerSec = m_pMPAHeader->GetBytesPerSecond(); + } +} + +bool CMPAFile::GetNextFrame() +{ + uint32 dwOffset = m_pMPAHeader->m_dwSyncOffset + m_pMPAHeader->m_dwRealFrameSize; + try + { + CMPAHeader* pFrame = new CMPAHeader( this, dwOffset, false ); + + delete m_pMPAHeader; + m_pMPAHeader = pFrame; + if( m_dwFrameNo > 0 ) + m_dwFrameNo++; + } + catch(...) + { + return false; + } + return true; +} + +bool CMPAFile::GetPrevFrame() +{ + uint32 dwOffset = m_pMPAHeader->m_dwSyncOffset-MPA_HEADER_SIZE; + try + { + // look backward from dwOffset on + CMPAHeader* pFrame = new CMPAHeader( this, dwOffset, false, true ); + + delete m_pMPAHeader; + m_pMPAHeader = pFrame; + if( m_dwFrameNo > 0 ) + m_dwFrameNo --; + } + catch(...) + { + return false; + } + + return true; +} + +bool CMPAFile::GetFirstFrame() +{ + uint32 dwOffset = 0; + try + { + CMPAHeader* pFrame = new CMPAHeader( this, dwOffset, false ); + + delete m_pMPAHeader; + m_pMPAHeader = pFrame; + m_dwFrameNo = 1; + } + catch(...) + { + return false; + } + return true; +} + +bool CMPAFile::GetLastFrame() +{ + uint32 dwOffset = m_dwEnd - m_dwBegin - MPA_HEADER_SIZE; + try + { + // look backward from dwOffset on + CMPAHeader* pFrame = new CMPAHeader( this, dwOffset, false, true ); + + delete m_pMPAHeader; + m_pMPAHeader = pFrame; + m_dwFrameNo = 0; + } + catch(...) + { + return false; + } + + return true; +} + +// destructor +CMPAFile::~CMPAFile(void) +{ + delete m_pMPAHeader; + + if( m_pVBRHeader ) + delete m_pVBRHeader; + + if( m_pBuffer ) + delete[] m_pBuffer; + + // close file + if( m_bMustReleaseFile ) + g_pFullFileSystem->Close( m_hFile ); + + if( m_szFile ) + free( (void*)m_szFile ); +} + +// open file +void CMPAFile::Open( const char * szFilename ) +{ + // open with CreateFile (no limitation of 128byte filename length, like in mmioOpen) + m_hFile = g_pFullFileSystem->Open( szFilename, "rb", "GAME" );//::CreateFile( szFilename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); + if( m_hFile == FILESYSTEM_INVALID_HANDLE ) + { + // throw error + throw CMPAException( CMPAException::ErrOpenFile, szFilename, _T("CreateFile"), true ); + } +} + +// set file position +void CMPAFile::SetPosition( int offset ) +{ + /* + LARGE_INTEGER liOff; + + liOff.QuadPart = lOffset; + liOff.LowPart = ::SetFilePointer(m_hFile, liOff.LowPart, &liOff.HighPart, dwMoveMethod ); + if (liOff.LowPart == INVALID_SET_FILE_POINTER && GetLastError() != NO_ERROR ) + { + // throw error + throw CMPAException( CMPAException::ErrSetPosition, m_szFile, _T("SetFilePointer"), true ); + } + */ + + g_pFullFileSystem->Seek( m_hFile, offset, FILESYSTEM_SEEK_HEAD ); +} + +// read from file, return number of bytes read +uint32 CMPAFile::Read( void *pData, uint32 dwSize, uint32 dwOffset ) +{ + uint32 dwBytesRead = 0; + + // set position first + SetPosition( m_dwBegin+dwOffset ); + + //if( !::ReadFile( m_hFile, pData, dwSize, &dwBytesRead, NULL ) ) + // throw CMPAException( CMPAException::ErrReadFile, m_szFile, _T("ReadFile"), true ); + dwBytesRead = g_pFullFileSystem->Read( pData, dwSize, m_hFile ); + + return dwBytesRead; +} + +// convert from big endian to native format (Intel=little endian) and return as uint32 (32bit) +uint32 CMPAFile::ExtractBytes( uint32& dwOffset, uint32 dwNumBytes, bool bMoveOffset ) +{ + Assert( dwNumBytes > 0 ); + Assert( dwNumBytes <= 4 ); // max 4 byte + + // enough bytes in buffer, otherwise read from file + if( !m_pBuffer || ( ((int)(m_dwBufferSize - dwOffset)) < (int)dwNumBytes) ) + FillBuffer( dwOffset + dwNumBytes ); + + uint32 dwResult = 0; + + // big endian extract (most significant byte first) (will work on little and big-endian computers) + uint32 dwNumByteShifts = dwNumBytes - 1; + + for( uint32 n=dwOffset; n < dwOffset+dwNumBytes; n++ ) + { + dwResult |= ((byte)m_pBuffer[n]) << (8*dwNumByteShifts); // the bit shift will do the correct byte order for you + dwNumByteShifts--; + } + + if( bMoveOffset ) + dwOffset += dwNumBytes; + + return dwResult; +} + +// throws exception if not possible +void CMPAFile::FillBuffer( uint32 dwOffsetToRead ) +{ + uint32 dwNewBufferSize; + + // calc new buffer size + if( m_dwBufferSize == 0 ) + dwNewBufferSize = m_dwInitBufferSize; + else + dwNewBufferSize = m_dwBufferSize*2; + + // is it big enough? + if( dwNewBufferSize < dwOffsetToRead ) + dwNewBufferSize = dwOffsetToRead; + + // reserve new buffer + BYTE* pNewBuffer = new BYTE[dwNewBufferSize]; + + // take over data from old buffer + if( m_pBuffer ) + { + memcpy( pNewBuffer, m_pBuffer, m_dwBufferSize ); + + // release old buffer + delete[] m_pBuffer; + } + m_pBuffer = (char*)pNewBuffer; + + // read <dwNewBufferSize-m_dwBufferSize> bytes from offset <m_dwBufferSize> + uint32 dwBytesRead = Read( m_pBuffer+m_dwBufferSize, dwNewBufferSize-m_dwBufferSize, m_dwBufferSize ); + + // no more bytes in buffer than read out from file + m_dwBufferSize += dwBytesRead; +} + +// Uses mp3 code from: http://www.codeproject.com/audio/MPEGAudioInfo.asp + +struct MP3Duration_t +{ + FileNameHandle_t h; + float duration; + + static bool LessFunc( const MP3Duration_t& lhs, const MP3Duration_t& rhs ) + { + return lhs.h < rhs.h; + } +}; + +CUtlRBTree< MP3Duration_t, int > g_MP3Durations( 0, 0, MP3Duration_t::LessFunc ); + +float GetMP3Duration_Helper( char const *filename ) +{ + float duration = 60.0f; + + // See if it's in the RB tree already... + char fn[ 512 ]; + Q_snprintf( fn, sizeof( fn ), "sound/%s", PSkipSoundChars( filename ) ); + + FileNameHandle_t h = g_pFullFileSystem->FindOrAddFileName( fn ); + + MP3Duration_t search; + search.h = h; + + int idx = g_MP3Durations.Find( search ); + if ( idx != g_MP3Durations.InvalidIndex() ) + { + return g_MP3Durations[ idx ].duration; + } + + try + { + CMPAFile MPAFile( fn, 0 ); + if ( MPAFile.m_dwBytesPerSec != 0 ) + { + duration = (float)(MPAFile.m_dwEnd - MPAFile.m_dwBegin) / (float)MPAFile.m_dwBytesPerSec; + } + } + catch ( ... ) + { + } + + search.duration = duration; + g_MP3Durations.Insert( search ); + + return duration; +} diff --git a/engine/audio/private/MPAFile.h b/engine/audio/private/MPAFile.h new file mode 100644 index 0000000..92d15ff --- /dev/null +++ b/engine/audio/private/MPAFile.h @@ -0,0 +1,125 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Uses mp3 code from: http://www.codeproject.com/audio/MPEGAudioInfo.asp +// +// There don't appear to be any licensing restrictions for using this code: +// +/* +- Readme - MPEG Audio Info Tool V2.0 - 2004-11-01 + +Description: +This tool can display information about MPEG audio files. It supports +MPEG1, MPEG2, MPEG2.5 in all three layers. You can get all the fields +from the MPEG audio frame in each frame of the file. Additionally you +can check the whole file for inconsistencies. + + +This tool was written as an example on how to use the classes: +CMPAFile, CMPAHeader, CVBRHeader and CMPAException. + +The article MPEG Audio Frame Header on Sourceproject +[http://www.codeproject.com/audio/MPEGAudioInfo.asp] +provides additional information about these classes and the frame header +in general. + +This tool was written with MS Visual C++ 7.1. The MFC library is +statically linked. +*/ +//============================================================================= + +#ifndef MPAFILE_H +#define MPAFILE_H +#ifdef _WIN32 +#pragma once +#endif + +#pragma once + +#include "VBRHeader.h" +#include "MPAHeader.h" +#include "filesystem.h" + +// exception class +class CMPAException +{ +public: + + enum ErrorIDs + { + ErrOpenFile, + ErrSetPosition, + ErrReadFile, + EndOfBuffer, + NoVBRHeader, + IncompleteVBRHeader, + NoFrameInTolerance, + NoFrame + }; + + CMPAException( ErrorIDs ErrorID, const char *szFile, const char *szFunction = NULL, bool bGetLastError=false ); + // copy constructor (necessary because of LPSTR members) + CMPAException(const CMPAException& Source); + ~CMPAException(void); + + ErrorIDs GetErrorID() { return m_ErrorID; } + + void ShowError(); + +private: + ErrorIDs m_ErrorID; + bool m_bGetLastError; + const char *m_szFunction; + const char *m_szFile; +}; + + +class CMPAFile +{ +public: + CMPAFile( const char *szFile, uint32 dwFileOffset, FileHandle_t hFile = FILESYSTEM_INVALID_HANDLE ); + ~CMPAFile(void); + + uint32 ExtractBytes( uint32 &dwOffset, uint32 dwNumBytes, bool bMoveOffset = true ); + const char *GetFilename() const { return m_szFile; }; + + bool GetNextFrame(); + bool GetPrevFrame(); + bool GetFirstFrame(); + bool GetLastFrame(); + +private: + static const uint32 m_dwInitBufferSize; + + // methods for file access + void Open( const char *szFilename ); + void SetPosition( int offset ); + uint32 Read( void *pData, uint32 dwSize, uint32 dwOffset ); + + void FillBuffer( uint32 dwOffsetToRead ); + + static uint32 m_dwBufferSizes[MAXTIMESREAD]; + + // concerning file itself + FileHandle_t m_hFile; + const char *m_szFile; + bool m_bMustReleaseFile; + +public: + uint32 m_dwBegin; // offset of first MPEG Audio frame + uint32 m_dwEnd; // offset of last MPEG Audio frame (estimated) + bool m_bVBRFile; + + uint32 m_dwBytesPerSec; + + CMPAHeader* m_pMPAHeader; + uint32 m_dwFrameNo; + + CVBRHeader* m_pVBRHeader; // XING or VBRI + + // concerning read-buffer + uint32 m_dwNumTimesRead; + char *m_pBuffer; + uint32 m_dwBufferSize; +}; + +#endif // MPAFILE_H diff --git a/engine/audio/private/MPAHeader.cpp b/engine/audio/private/MPAHeader.cpp new file mode 100644 index 0000000..6a8357c --- /dev/null +++ b/engine/audio/private/MPAHeader.cpp @@ -0,0 +1,332 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#if defined( WIN32) && !defined( _X360 ) +#include "winlite.h" +#endif +#include "tier0/platform.h" +#include "MPAFile.h" + + +// static variables +const char *CMPAHeader::m_szLayers[] = { "Layer I", "Layer II", "Layer III" }; +const char *CMPAHeader::m_szMPEGVersions[] = {"MPEG 2.5", "", "MPEG 2", "MPEG 1" }; +const char *CMPAHeader::m_szChannelModes[] = { "Stereo", "Joint Stereo", "Dual Channel", "Single Channel" }; +const char *CMPAHeader::m_szEmphasis[] = { "None", "50/15ms", "", "CCIT J.17" }; + +// tolerance range, look at expected offset +/- m_dwTolerance for subsequent frames +const uint32 CMPAHeader::m_dwTolerance = 3; // 3 bytes + +// max. range where to look for frame sync +const uint32 CMPAHeader::m_dwMaxRange = ( 256 * 1024 ); + +// sampling rates in hertz: 1. index = MPEG Version ID, 2. index = sampling rate index +const uint32 CMPAHeader::m_dwSamplingRates[4][3] = +{ + {11025, 12000, 8000, }, // MPEG 2.5 + {0, 0, 0, }, // reserved + {22050, 24000, 16000, }, // MPEG 2 + {44100, 48000, 32000 } // MPEG 1 +}; + +// padding sizes in bytes for different layers: 1. index = layer +const uint32 CMPAHeader::m_dwPaddingSizes[3] = +{ + 4, // Layer1 + 1, // Layer2 + 1 // Layer3 +}; + +// bitrates: 1. index = LSF, 2. index = Layer, 3. index = bitrate index +const uint32 CMPAHeader::m_dwBitrates[2][3][15] = +{ + { // MPEG 1 + {0,32,64,96,128,160,192,224,256,288,320,352,384,416,448,}, // Layer1 + {0,32,48,56, 64, 80, 96,112,128,160,192,224,256,320,384,}, // Layer2 + {0,32,40,48, 56, 64, 80, 96,112,128,160,192,224,256,320,} // Layer3 + }, + { // MPEG 2, 2.5 + {0,32,48,56,64,80,96,112,128,144,160,176,192,224,256,}, // Layer1 + {0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,}, // Layer2 + {0,8,16,24,32,40,48,56,64,80,96,112,128,144,160,} // Layer3 + } +}; + +// Samples per Frame: 1. index = LSF, 2. index = Layer +const uint32 CMPAHeader::m_dwSamplesPerFrames[2][3] = +{ + { // MPEG 1 + 384, // Layer1 + 1152, // Layer2 + 1152 // Layer3 + }, + { // MPEG 2, 2.5 + 384, // Layer1 + 1152, // Layer2 + 576 // Layer3 + } +}; + +// Samples per Frame / 8 +const uint32 CMPAHeader::m_dwCoefficients[2][3] = +{ + { // MPEG 1 + 48, // Layer1 + 144, // Layer2 + 144 // Layer3 + }, + { // MPEG 2, 2.5 + 48, // Layer1 + 144, // Layer2 + 72 // Layer3 + } +}; + +// needed later for CRC check +// sideinformation size: 1.index = lsf, 2. index = layer, 3. index = mono +const uint32 CMPAHeader::m_dwSideinfoSizes[2][3][2] = +{ + { // MPEG 1 (not mono, mono + {0,0}, // Layer1 + {0,0}, // Layer2 + {9,17} // Layer3 + }, + { // MPEG 2, 2.5 + {0,0}, // Layer1 + {0,0}, // Layer2 + {17,32} // Layer3 + } +}; + +// constructor (throws exception if no frame found) +CMPAHeader::CMPAHeader( CMPAFile* pMPAFile, uint32 dwExpectedOffset, bool bSubsequentFrame, bool bReverse ) : +m_pMPAFile( pMPAFile ), m_dwSyncOffset( dwExpectedOffset ), m_dwRealFrameSize( 0 ) +{ + // first check at expected offset (extended for not subsequent frames) + HeaderError error = IsSync( m_dwSyncOffset, !bSubsequentFrame ); + int nStep=1; + int nSyncOffset; + + while( error != noError ) + { + // either look in tolerance range + if( bSubsequentFrame ) + { + if( nStep > m_dwTolerance ) + { + // out of tolerance range + throw CMPAException( CMPAException::NoFrameInTolerance, pMPAFile->GetFilename() ? pMPAFile->GetFilename() : "??" ); + } + + // look around dwExpectedOffset with increasing steps (+1,-1,+2,-2,...) + if( m_dwSyncOffset <= dwExpectedOffset ) + { + nSyncOffset = dwExpectedOffset + nStep; + } + else + { + nSyncOffset = dwExpectedOffset - nStep++; + } + } + // just go forward/backward to find sync + else + { + nSyncOffset = ((int)m_dwSyncOffset) + (bReverse?-1:+1); + } + + // is new offset within valid range? + if( nSyncOffset < 0 || nSyncOffset > (int)((pMPAFile->m_dwEnd - pMPAFile->m_dwBegin) - MPA_HEADER_SIZE) || abs( (long)(nSyncOffset-dwExpectedOffset) ) > m_dwMaxRange ) + { + // out of tolerance range + throw CMPAException( CMPAException::NoFrame, pMPAFile->GetFilename() ? pMPAFile->GetFilename() : "??" ); + } + m_dwSyncOffset = nSyncOffset; + + // found sync? + error = IsSync( m_dwSyncOffset, !bSubsequentFrame ); + } +} + +// destructor +CMPAHeader::~CMPAHeader() +{ +} + +// skips first 32kbit/s or lower bitrate frames to estimate bitrate (returns true if bitrate is variable) +bool CMPAHeader::SkipEmptyFrames() +{ + if( m_dwBitrate > 32 ) + return false; + + uint32 dwHeader; + try + { + while( m_dwBitrate <= 32 ) + { + m_dwSyncOffset += m_dwComputedFrameSize + MPA_HEADER_SIZE; + dwHeader = m_pMPAFile->ExtractBytes( m_dwSyncOffset, MPA_HEADER_SIZE, false ); + + if( IsSync( dwHeader, false ) != noError ) + return false; + } + } + catch(CMPAException& /*Exc*/) // just catch the exception and return false + { + return false; + } + return true; +} + +// in dwHeader stands 32bit header in big-endian format: frame sync at the end! +// because shifts do only work for integral types!!! +CMPAHeader::HeaderError CMPAHeader::DecodeHeader( uint32 dwHeader, bool bSimpleDecode ) +{ + // Check SYNC bits (last eleven bits set) + if( (dwHeader >> 24 != 0xff) || ((((dwHeader >> 16))&0xe0) != 0xe0) ) + return noSync; + + // get MPEG version + m_Version = (MPAVersion)((dwHeader >> 19) & 0x03); // mask only the rightmost 2 bits + if( m_Version == MPEGReserved ) + return headerCorrupt; + + if( m_Version == MPEG1 ) + m_bLSF = false; + else + m_bLSF = true; + + // get layer (0 = layer1, 2 = layer2, ...) + m_Layer = (MPALayer)(3 - ((dwHeader >> 17) & 0x03)); + if( m_Layer == LayerReserved ) + return headerCorrupt; + + // protection bit (inverted) + m_bCRC = !((dwHeader >> 16) & 0x01); + + // bitrate + BYTE bIndex = (BYTE)((dwHeader >> 12) & 0x0F); + if( bIndex == 0x0F ) // all bits set is reserved + return headerCorrupt; + m_dwBitrate = m_dwBitrates[m_bLSF][m_Layer][bIndex] * 1000; // convert from kbit to bit + + if( m_dwBitrate == 0 ) // means free bitrate (is unsupported yet) + return freeBitrate; + + // sampling rate + bIndex = (BYTE)((dwHeader >> 10) & 0x03); + if( bIndex == 0x03 ) // all bits set is reserved + return headerCorrupt; + m_dwSamplesPerSec = m_dwSamplingRates[m_Version][bIndex]; + + // padding bit + m_dwPaddingSize = m_dwPaddingSizes[m_Layer] * ((dwHeader >> 9) & 0x01); + + // calculate frame size + m_dwComputedFrameSize = (m_dwCoefficients[m_bLSF][m_Layer] * m_dwBitrate / m_dwSamplesPerSec) + m_dwPaddingSize; + m_dwSamplesPerFrame = m_dwSamplesPerFrames[m_bLSF][m_Layer]; + + if( !bSimpleDecode ) + { + // private bit + m_bPrivate = (dwHeader >> 8) & 0x01; + + // channel mode + m_ChannelMode = (ChannelMode)((dwHeader >> 6) & 0x03); + + // mode extension (currently not used) + m_ModeExt = (BYTE)((dwHeader >> 4) & 0x03); + + // copyright bit + m_bCopyright = (dwHeader >> 3) & 0x01; + + // original bit + m_bCopyright = (dwHeader >> 2) & 0x01; + + // emphasis + m_Emphasis = (Emphasis)(dwHeader & 0x03); + if( m_Emphasis == EmphReserved ) + return headerCorrupt; + } + return noError; +} + +CMPAHeader::HeaderError CMPAHeader::IsSync( uint32 dwOffset, bool bExtended ) +{ + HeaderError error = noSync; + uint32 dwHeader = m_pMPAFile->ExtractBytes( dwOffset, MPA_HEADER_SIZE, false ); + + // sync bytes found? + if( (dwHeader & 0xFFE00000) == 0xFFE00000 ) + { + error = DecodeHeader( dwHeader ); + if( error == noError ) + { + // enough buffer to do extended check? + if( bExtended ) + { + // recursive call (offset for next frame header) + dwOffset = m_dwSyncOffset+m_dwComputedFrameSize; + try + { + CMPAHeader m_SubsequentFrame( m_pMPAFile, dwOffset, true ); + m_dwRealFrameSize = m_SubsequentFrame.m_dwSyncOffset - m_dwSyncOffset; + } + catch( CMPAException& Exc ) + { + // could not find any subsequent frame, assume it is the last frame + if( Exc.GetErrorID() == CMPAException::NoFrame ) + { + if( dwOffset + m_pMPAFile->m_dwBegin > m_pMPAFile->m_dwEnd ) + m_dwRealFrameSize = m_pMPAFile->m_dwEnd - m_pMPAFile->m_dwBegin - m_dwSyncOffset; + else + m_dwRealFrameSize = m_dwComputedFrameSize; + error = noError; + } + else + error = noSync; + } + } + } + } + return error; +} + +// CRC-16 lookup table +const uint16 CMPAHeader::wCRC16Table[256] = +{ + 0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241, + 0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440, + 0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40, + 0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841, + 0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40, + 0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41, + 0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641, + 0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040, + 0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240, + 0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441, + 0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41, + 0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840, + 0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41, + 0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40, + 0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640, + 0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041, + 0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240, + 0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441, + 0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41, + 0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840, + 0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41, + 0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40, + 0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640, + 0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041, + 0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241, + 0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440, + 0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40, + 0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841, + 0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40, + 0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41, + 0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641, + 0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040 +}; diff --git a/engine/audio/private/MPAHeader.h b/engine/audio/private/MPAHeader.h new file mode 100644 index 0000000..3810124 --- /dev/null +++ b/engine/audio/private/MPAHeader.h @@ -0,0 +1,116 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef MPAHEADER_H +#define MPAHEADER_H +#ifdef _WIN32 +#pragma once +#endif + +#pragma once + +#define MPA_HEADER_SIZE 4 // MPEG-Audio Header Size 32bit +#define MAXTIMESREAD 5 + +class CMPAFile; + +class CMPAHeader +{ +public: + CMPAHeader( CMPAFile* pMPAFile, uint32 dwExpectedOffset = 0, bool bSubsequentFrame = false, bool bReverse = false ); + ~CMPAHeader(); + + bool SkipEmptyFrames(); + + // bitrate is in bit per second, to calculate in bytes => (/ 8) + uint32 GetBytesPerSecond() const { return m_dwBitrate / 8; }; + // calc number of seconds from number of frames + uint32 GetLengthSecond(uint32 dwNumFrames) const { return dwNumFrames * m_dwSamplesPerFrame / m_dwSamplesPerSec; }; + uint32 GetBytesPerSecond( uint32 dwNumFrames, uint32 dwNumBytes ) const { return dwNumBytes / GetLengthSecond( dwNumFrames ); }; + bool IsMono() const { return (m_ChannelMode == SingleChannel)?true:false; }; + // true if MPEG2/2.5 otherwise false + bool IsLSF() const { return m_bLSF; }; + +private: + static const uint32 m_dwMaxRange; + static const uint32 m_dwTolerance; + static const uint32 m_dwSamplingRates[4][3]; + static const uint32 m_dwPaddingSizes[3]; + static const uint32 m_dwBitrates[2][3][15]; + static const uint32 m_dwSamplesPerFrames[2][3]; + static const uint32 m_dwCoefficients[2][3]; + + // necessary for CRC check (not yet implemented) + static const uint32 m_dwSideinfoSizes[2][3][2]; + static const uint16 wCRC16Table[256]; + + bool m_bLSF; // true means lower sampling frequencies (=MPEG2/MPEG2.5) + CMPAFile* m_pMPAFile; + +public: + static const char * m_szLayers[]; + static const char * m_szMPEGVersions[]; + static const char * m_szChannelModes[]; + static const char * m_szEmphasis[]; + + enum MPAVersion + { + MPEG25 = 0, + MPEGReserved, + MPEG2, + MPEG1 + }m_Version; + + enum MPALayer + { + Layer1 = 0, + Layer2, + Layer3, + LayerReserved + }m_Layer; + + enum Emphasis + { + EmphNone = 0, + Emph5015, + EmphReserved, + EmphCCITJ17 + }m_Emphasis; + + enum ChannelMode + { + Stereo, + JointStereo, + DualChannel, + SingleChannel + }m_ChannelMode; + + uint32 m_dwSamplesPerSec; + uint32 m_dwSamplesPerFrame; + uint32 m_dwBitrate; // in bit per second (1 kb = 1000 bit, not 1024) + uint32 m_dwSyncOffset; + uint32 m_dwComputedFrameSize, m_dwRealFrameSize; + uint32 m_dwPaddingSize; + + // flags + bool m_bCopyright, m_bPrivate, m_bOriginal; + bool m_bCRC; + uint8 m_ModeExt; + +private: + enum HeaderError + { + noError, + noSync, + freeBitrate, + headerCorrupt + }; + + HeaderError DecodeHeader( uint32 dwHeader, bool bSimpleDecode = false ); + inline HeaderError IsSync( uint32 dwOffset, bool bExtended ); +}; + +#endif // MPAHEADER_H diff --git a/engine/audio/private/VBRHeader.cpp b/engine/audio/private/VBRHeader.cpp new file mode 100644 index 0000000..ce554d4 --- /dev/null +++ b/engine/audio/private/VBRHeader.cpp @@ -0,0 +1,304 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#include "audio_pch.h" +#include "tier0/platform.h" +#include "MPAFile.h" // also includes vbrheader.h +#include "tier0/dbg.h" + +#ifndef MAKEFOURCC + #define MAKEFOURCC(ch0, ch1, ch2, ch3) \ + ((uint32)(BYTE)(ch0) | ((uint32)(BYTE)(ch1) << 8) | \ + ((uint32)(BYTE)(ch2) << 16) | ((uint32)(BYTE)(ch3) << 24 )) +#endif //defined(MAKEFOURCC) + +// XING Header offset: 1. index = lsf, 2. index = mono +uint32 CVBRHeader::m_dwXINGOffsets[2][2] = +{ + // MPEG 1 (not mono, mono) + { 32 + MPA_HEADER_SIZE, 17 + MPA_HEADER_SIZE }, + // MPEG 2/2.5 + { 17 + MPA_HEADER_SIZE, 9 + MPA_HEADER_SIZE } +}; + +// first test with this static method, if it does exist +bool CVBRHeader::IsVBRHeaderAvailable( CMPAFile* pMPAFile, VBRHeaderType& HeaderType, uint32& dwOffset ) +{ + Assert(pMPAFile); + + // where does VBR header begin (XING) + uint32 dwNewOffset = dwOffset + m_dwXINGOffsets[pMPAFile->m_pMPAHeader->IsLSF()][pMPAFile->m_pMPAHeader->IsMono()]; + + // check for XING header first + if( CheckXING( pMPAFile, dwNewOffset ) ) + { + HeaderType = XINGHeader; + // seek offset back to header begin + dwOffset = dwNewOffset - 4; + return true; + } + + // VBRI header always at fixed offset + dwNewOffset = dwOffset + 32 + MPA_HEADER_SIZE; + if( CheckVBRI( pMPAFile, dwNewOffset ) ) + { + HeaderType = VBRIHeader; + // seek offset back to header begin + dwOffset = dwNewOffset - 4; + return true; + } + HeaderType = NoHeader; + return false; +} + +CVBRHeader::CVBRHeader( CMPAFile* pMPAFile, VBRHeaderType HeaderType, uint32 dwOffset ) : + m_pMPAFile( pMPAFile ), m_pnToc(NULL), m_HeaderType( HeaderType ), m_dwOffset(dwOffset), m_dwFrames(0), m_dwBytes(0) +{ + switch( m_HeaderType ) + { + case NoHeader: + // no Header found + throw CMPAException( CMPAException::NoVBRHeader, pMPAFile->GetFilename(), NULL, false ); + break; + case XINGHeader: + if( !ExtractXINGHeader( m_dwOffset ) ) + throw CMPAException( CMPAException::NoVBRHeader, pMPAFile->GetFilename(), NULL, false ); + break; + case VBRIHeader: + if( !ExtractVBRIHeader( m_dwOffset ) ) + throw CMPAException( CMPAException::NoVBRHeader, pMPAFile->GetFilename(), NULL, false ); + break; + } + // calc bitrate + if( m_dwBytes > 0 && m_dwFrames > 0 ) + { + // calc number of seconds + m_dwBytesPerSec = m_pMPAFile->m_pMPAHeader->GetBytesPerSecond( m_dwFrames, m_dwBytes ); + } + else // incomplete header found + { + throw CMPAException( CMPAException::IncompleteVBRHeader, pMPAFile->GetFilename(), NULL, false ); + } +} + +bool CVBRHeader::CheckID( CMPAFile* pMPAFile, char ch0, char ch1, char ch2, char ch3, uint32& dwOffset ) +{ + return ( pMPAFile->ExtractBytes( dwOffset, 4 ) == MAKEFOURCC( ch3, ch2, ch1, ch0 ) ); +} + +bool CVBRHeader::CheckXING( CMPAFile* pMPAFile, uint32& dwOffset ) +{ + // XING ID found? + if( !CheckID( pMPAFile, 'X', 'i', 'n', 'g', dwOffset) && !CheckID( pMPAFile, 'I', 'n', 'f', 'o', dwOffset) ) + return false; + return true; +} + +bool CVBRHeader::CheckVBRI( CMPAFile* pMPAFile, uint32& dwOffset ) +{ + // VBRI ID found? + if( !CheckID( pMPAFile, 'V', 'B', 'R', 'I', dwOffset ) ) + return false; + return true; +} + + +// currently not used +bool CVBRHeader::ExtractLAMETag( uint32 dwOffset ) +{ + // LAME ID found? + if( !CheckID( m_pMPAFile, 'L', 'A', 'M', 'E', dwOffset ) && !CheckID( m_pMPAFile, 'G', 'O', 'G', 'O', dwOffset ) ) + return false; + + return true; +} + +bool CVBRHeader::ExtractXINGHeader( uint32 dwOffset ) +{ + /* XING VBR-Header + + size description + 4 'Xing' or 'Info' + 4 flags (indicates which fields are used) + 4 frames (optional) + 4 bytes (optional) + 100 toc (optional) + 4 a VBR quality indicator: 0=best 100=worst (optional) + + */ + if( !CheckXING( m_pMPAFile, dwOffset ) ) + return false; + + uint32 dwFlags; + + // get flags (mandatory in XING header) + dwFlags = m_pMPAFile->ExtractBytes( dwOffset, 4 ); + + // extract total number of frames in file + if(dwFlags & FRAMES_FLAG) + m_dwFrames = m_pMPAFile->ExtractBytes(dwOffset,4); + + // extract total number of bytes in file + if(dwFlags & BYTES_FLAG) + m_dwBytes = m_pMPAFile->ExtractBytes(dwOffset,4); + + // extract TOC (for more accurate seeking) + if (dwFlags & TOC_FLAG) + { + m_dwTableSize = 100; + m_pnToc = new int[m_dwTableSize]; + + if( m_pnToc ) + { + for(uint32 i=0;i<m_dwTableSize;i++) + m_pnToc[i] = m_pMPAFile->ExtractBytes( dwOffset, 1 ); + } + } + + m_dwQuality = (uint32)-1; + if(dwFlags & VBR_SCALE_FLAG ) + m_dwQuality = m_pMPAFile->ExtractBytes(dwOffset, 4); + + return true; +} + +bool CVBRHeader::ExtractVBRIHeader( uint32 dwOffset ) +{ + /* FhG VBRI Header + + size description + 4 'VBRI' (ID) + 2 version + 2 delay + 2 quality + 4 # bytes + 4 # frames + 2 table size (for TOC) + 2 table scale (for TOC) + 2 size of table entry (max. size = 4 byte (must be stored in an integer)) + 2 frames per table entry + + ?? dynamic table consisting out of frames with size 1-4 + whole length in table size! (for TOC) + + */ + + if( !CheckVBRI( m_pMPAFile, dwOffset ) ) + return false; + + // extract all fields from header (all mandatory) + m_dwVersion = m_pMPAFile->ExtractBytes(dwOffset, 2 ); + m_fDelay = (float)m_pMPAFile->ExtractBytes(dwOffset, 2 ); + m_dwQuality = m_pMPAFile->ExtractBytes(dwOffset, 2 ); + m_dwBytes = m_pMPAFile->ExtractBytes(dwOffset, 4 ); + m_dwFrames = m_pMPAFile->ExtractBytes(dwOffset, 4 ); + m_dwTableSize = m_pMPAFile->ExtractBytes(dwOffset, 2 ) + 1; //!!! + m_dwTableScale = m_pMPAFile->ExtractBytes(dwOffset, 2 ); + m_dwBytesPerEntry = m_pMPAFile->ExtractBytes(dwOffset, 2 ); + m_dwFramesPerEntry = m_pMPAFile->ExtractBytes(dwOffset, 2 ); + + // extract TOC (for more accurate seeking) + m_pnToc = new int[m_dwTableSize]; + if( m_pnToc ) + { + for ( unsigned int i = 0 ; i < m_dwTableSize ; i++) + { + m_pnToc[i] = m_pMPAFile->ExtractBytes(dwOffset, m_dwBytesPerEntry ); + } + } + return true; +} + +CVBRHeader::~CVBRHeader(void) +{ + if( m_pnToc ) + delete[] m_pnToc; +} + +// get byte position for percentage value (fPercent) of file +bool CVBRHeader::SeekPoint(float fPercent, uint32& dwSeekPoint) +{ + if( !m_pnToc || m_dwBytes == 0 ) + return false; + + if( fPercent < 0.0f ) + fPercent = 0.0f; + if( fPercent > 100.0f ) + fPercent = 100.0f; + + switch( m_HeaderType ) + { + case XINGHeader: + dwSeekPoint = SeekPointXING( fPercent ); + break; + case VBRIHeader: + dwSeekPoint = SeekPointVBRI( fPercent ); + break; + } + return true; +} + +uint32 CVBRHeader::SeekPointXING(float fPercent) const +{ + // interpolate in TOC to get file seek point in bytes + int a; + float fa, fb, fx; + + a = (int)fPercent; + if( a > 99 ) a = 99; + fa = (float)m_pnToc[a]; + + if( a < 99 ) + { + fb = (float)m_pnToc[a+1]; + } + else + { + fb = 256.0f; + } + + fx = fa + (fb-fa)*(fPercent-a); + + uint32 dwSeekpoint = (int)((1.0f/256.0f)*fx*m_dwBytes); + return dwSeekpoint; +} + +uint32 CVBRHeader::SeekPointVBRI(float fPercent) const +{ + return SeekPointByTimeVBRI( (fPercent/100.0f) * m_pMPAFile->m_pMPAHeader->GetLengthSecond( m_dwFrames ) * 1000.0f ); +} + +uint32 CVBRHeader::SeekPointByTimeVBRI(float fEntryTimeMS) const +{ + unsigned int i=0, fraction = 0; + uint32 dwSeekPoint = 0; + + float fLengthMS; + float fLengthMSPerTOCEntry; + float fAccumulatedTimeMS = 0.0f ; + + fLengthMS = (float)m_pMPAFile->m_pMPAHeader->GetLengthSecond( m_dwFrames ) * 1000.0f ; + fLengthMSPerTOCEntry = fLengthMS / (float)m_dwTableSize; + + if ( fEntryTimeMS > fLengthMS ) + fEntryTimeMS = fLengthMS; + + while ( fAccumulatedTimeMS <= fEntryTimeMS ) + { + dwSeekPoint += m_pnToc[i++]; + fAccumulatedTimeMS += fLengthMSPerTOCEntry; + } + + // Searched too far; correct result + fraction = ( (int)(((( fAccumulatedTimeMS - fEntryTimeMS ) / fLengthMSPerTOCEntry ) + + (1.0f/(2.0f*(float)m_dwFramesPerEntry))) * (float)m_dwFramesPerEntry)); + + dwSeekPoint -= (uint32)((float)m_pnToc[i-1] * (float)(fraction) + / (float)m_dwFramesPerEntry); + + return dwSeekPoint; +} + diff --git a/engine/audio/private/VBRHeader.h b/engine/audio/private/VBRHeader.h new file mode 100644 index 0000000..42395cf --- /dev/null +++ b/engine/audio/private/VBRHeader.h @@ -0,0 +1,72 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//============================================================================= + +#ifndef VBRHEADER_H +#define VBRHEADER_H +#ifdef _WIN32 +#pragma once +#endif + +// for XING VBR Header flags +#define FRAMES_FLAG 0x0001 +#define BYTES_FLAG 0x0002 +#define TOC_FLAG 0x0004 +#define VBR_SCALE_FLAG 0x0008 + +class CMPAFile; + +class CVBRHeader +{ +public: + enum VBRHeaderType + { + NoHeader, + XINGHeader, + VBRIHeader + }; + + CVBRHeader( CMPAFile* pMPAFile, VBRHeaderType HeaderType, uint32 dwOffset ); + ~CVBRHeader(void); + + static bool IsVBRHeaderAvailable( CMPAFile* pMPAFile, VBRHeaderType& HeaderType, uint32& dwOffset ); + bool SeekPoint(float fPercent, uint32& dwSeekPoint); + + uint32 m_dwBytesPerSec; + uint32 m_dwBytes; // total number of bytes + uint32 m_dwFrames; // total number of frames + +private: + static uint32 m_dwXINGOffsets[2][2]; + + static bool CheckID( CMPAFile* pMPAFile, char ch0, char ch1, char ch2, char ch3, uint32& dwOffset ); + static bool CheckXING( CMPAFile* pMPAFile, uint32& dwOffset ); + static bool CheckVBRI( CMPAFile* pMPAFile, uint32& dwOffset ); + + bool ExtractLAMETag( uint32 dwOffset ); + bool ExtractXINGHeader( uint32 dwOffset ); + bool ExtractVBRIHeader( uint32 dwOffset ); + + uint32 SeekPointXING(float fPercent)const ; + uint32 SeekPointVBRI(float fPercent) const; + uint32 SeekPointByTimeVBRI(float fEntryTimeMS) const; + + CMPAFile* m_pMPAFile; +public: + VBRHeaderType m_HeaderType; + uint32 m_dwOffset; + uint32 m_dwQuality; // quality (0..100) + int* m_pnToc; // TOC points for seeking (must be freed) + uint32 m_dwTableSize; // size of table (number of entries) + + // only VBRI + float m_fDelay; + uint32 m_dwTableScale; // for seeking + uint32 m_dwBytesPerEntry; + uint32 m_dwFramesPerEntry; + uint32 m_dwVersion; +}; + +#endif // VBRHEADER_H diff --git a/engine/audio/private/circularbuffer.cpp b/engine/audio/private/circularbuffer.cpp new file mode 100644 index 0000000..8c188e3 --- /dev/null +++ b/engine/audio/private/circularbuffer.cpp @@ -0,0 +1,271 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Circular Buffer +// +//=============================================================================// + +#include "tier0/dbg.h" +#include "circularbuffer.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CCircularBuffer::CCircularBuffer() +{ + SetSize( 0 ); +} + +CCircularBuffer::CCircularBuffer(int size) +{ + SetSize(size); +} + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Sets the maximum size for a circular buffer. This does not do any +// memory allocation, it simply informs the buffer of its size. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +void CCircularBuffer::SetSize(int size) +{ + Assert( this ); + + m_nSize = size; + m_nRead = 0; + m_nWrite = 0; + m_nCount = 0; +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Empties a circular buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +void CCircularBuffer::Flush() +{ + AssertValid(); + + m_nRead = 0; + m_nWrite = 0; + m_nCount = 0; +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Returns the available space in a circular buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::GetWriteAvailable() +{ + AssertValid(); + + return(m_nSize - m_nCount); +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Returns the size of a circular buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::GetSize() +{ + AssertValid(); + + return(m_nSize); +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Returns the number of bytes in a circular buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::GetReadAvailable() +{ + AssertValid(); + + return(m_nCount); +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Reads a specified number of bytes from a circular buffer without +// consuming them. They will still be available for future calls to +// Read or Peek. +//Input : pchDest - destination buffer. +// m_nCount - number of bytes to place in destination buffer. +//Output : Returns the number of bytes placed in the destination buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::Peek(char *pchDest, int nCount) +{ + // If no data available, just return. + if(m_nCount == 0) + { + return(0); + } + + // + // Requested amount should not exceed the available amount. + // + nCount = MIN(m_nCount, nCount); + + // + // Copy as many of the requested bytes as possible. + // If buffer wrap occurs split the data into two chunks. + // + if (m_nRead + nCount > m_nSize) + { + int nCount1 = m_nSize - m_nRead; + memcpy(pchDest, &m_chData[m_nRead], nCount1); + pchDest += nCount1; + + int nCount2 = nCount - nCount1; + memcpy(pchDest, m_chData, nCount2); + } + // Otherwise copy it in one go. + else + { + memcpy(pchDest, &m_chData[m_nRead], nCount); + } + + AssertValid(); + return nCount; +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Advances the read index, consuming a specified number of bytes from +// the circular buffer. +//Input : m_nCount - number of bytes to consume. +//Output : Returns the actual number of bytes consumed. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::Advance(int nCount) +{ + // If no data available, just return. + if (m_nCount == 0) + { + return(0); + } + + // + // Requested amount should not exceed the available amount. + // + nCount = MIN(m_nCount, nCount); + + // Advance the read pointer, checking for buffer + //wrap. + // + m_nRead = (m_nRead + nCount) % m_nSize; + m_nCount -= nCount; + + // + // If we have emptied the buffer, reset the read and write indices + // to minimize buffer wrap. + // + if (m_nCount == 0) + { + m_nRead = 0; + m_nWrite = 0; + } + + AssertValid(); + return nCount; +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Reads a specified number of bytes from a circular buffer. The bytes +// will be consumed by the read process. +//Input : pchDest - destination buffer. +// m_nCount - number of bytes to place in destination buffer. +//Output : Returns the number of bytes placed in the destination buffer. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::Read(void *pchDestIn, int nCount) +{ + int nPeeked; + int nRead; + + char *pchDest = (char*)pchDestIn; + + nPeeked = Peek(pchDest, nCount); + + if (nPeeked != 0) + { + nRead = Advance(nPeeked); + + assert( nRead == nPeeked); + } + else + { + nRead = 0; + } + + AssertValid(); + return(nRead); +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +//Purpose : Writes a specified number of bytes to the buffer. +//Input : pm_chData - buffer containing bytes to bw written. +// m_nCount - the number of bytes to write. +//Output : Returns the number of bytes written. If there wa insufficient space +// to write all requested bytes, the value returned will be less than +// the requested amount. +//Author : DSpeyrer +//------------------------------------------------------------------------------ +int CCircularBuffer::Write(void *pData, int nBytesRequested) +{ + // Write all the data. + int nBytesToWrite = nBytesRequested; + char *pDataToWrite = (char*)pData; + + while(nBytesToWrite) + { + int from = m_nWrite; + int to = m_nWrite + nBytesToWrite; + + if(to >= m_nSize) + { + to = m_nSize; + } + + memcpy(&m_chData[from], pDataToWrite, to - from); + pDataToWrite += to - from; + + m_nWrite = to % m_nSize; + nBytesToWrite -= to - from; + } + + // Did it cross the read pointer? Then slide the read pointer up. + // This way, we will discard the old data. + if(nBytesRequested > (m_nSize - m_nCount)) + { + m_nCount = m_nSize; + m_nRead = m_nWrite; + } + else + { + m_nCount += nBytesRequested; + } + + AssertValid(); + return nBytesRequested; +} + +CCircularBuffer *AllocateCircularBuffer( int nSize ) +{ + char *pBuff = (char *)malloc( sizeof( CCircularBuffer ) + nSize - 1 ); + + CCircularBuffer *pCCircularBuffer = (CCircularBuffer *)pBuff; + + pCCircularBuffer->SetSize( nSize ); + return pCCircularBuffer; +} + +void FreeCircularBuffer( CCircularBuffer *pCircularBuffer ) +{ + free( (char*)pCircularBuffer ); +} + diff --git a/engine/audio/private/circularbuffer.h b/engine/audio/private/circularbuffer.h new file mode 100644 index 0000000..d24f04d --- /dev/null +++ b/engine/audio/private/circularbuffer.h @@ -0,0 +1,99 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Defines an interface for circular buffers. Data can be written to +// and read from these buffers as though from a file. When it is +// write-overflowed (you write more data in than the buffer can hold), +// the read pointer is advanced to allow the new data to be written. +// This means old data will be discarded if you write too much data +// into the buffer. +// +// MikeD: Moved all the functions into a class. +// Changed it so when the buffer overflows, the old data +// is discarded rather than the new. +// +//=====================================================================================// + +#ifndef CIRCULARBUFFER_H +#define CIRCULARBUFFER_H + +#pragma once + +class CCircularBuffer +{ +public: + CCircularBuffer(); + CCircularBuffer( int size ); + void SetSize( int nSize ); + +protected: + inline void AssertValid() + { +#ifdef _DEBUG + Assert( this ); + Assert( m_nSize > 0 ); + Assert( m_nCount >= 0 ); + Assert( m_nCount <= m_nSize ); + Assert( m_nWrite < m_nSize ); + + // Verify that m_nCount is correct. + if( m_nRead == m_nWrite ) + { + Assert( m_nCount == 0 || m_nCount == m_nSize ); + } + else + { + int testCount=0; + if ( m_nRead < m_nWrite ) + testCount = m_nWrite - m_nRead; + else + testCount = (m_nSize - m_nRead) + m_nWrite; + + Assert( testCount == m_nCount ); + } +#endif + } + +public: + + void Flush(); + int GetSize(); // Get the size of the buffer (how much can you write without reading + // before losing data. + + int GetWriteAvailable(); // Get the amount available to write without overflowing. + // Note: you can write however much you want, but it may overflow, + // in which case the newest data is kept and the oldest is discarded. + + int GetReadAvailable(); // Get the amount available to read. + + int GetMaxUsed(); + int Peek(char *pchDest, int nCount); + int Advance(int nCount); + int Read(void *pchDest, int nCount); + int Write(void *pchData, int nCount); + +public: + int m_nCount; // Space between the read and write pointers (how much data we can read). + + int m_nRead; // Read index into circular buffer + int m_nWrite; // Write index into circular buffer + + int m_nSize; // Size of circular buffer in bytes (how much data it can hold). + char m_chData[1]; // Circular buffer holding data +}; + + +// Use this to instantiate a CircularBuffer. +template< int size > +class CSizedCircularBuffer : public CCircularBuffer +{ +public: + CSizedCircularBuffer() : CCircularBuffer(size) {} + +private: + char myData[size-1]; +}; + +CCircularBuffer *AllocateCircularBuffer( int nSize ); +void FreeCircularBuffer( CCircularBuffer *pCircularBuffer ); + +#endif // CIRCULARBUFFER_H
\ No newline at end of file diff --git a/engine/audio/private/eax.h b/engine/audio/private/eax.h new file mode 100644 index 0000000..82051e9 --- /dev/null +++ b/engine/audio/private/eax.h @@ -0,0 +1,124 @@ +// EAX.H -- DirectSound Environmental Audio Extensions + +#ifndef EAX_H +#define EAX_H +#pragma once + + +// EAX (listener) reverb property set {4a4e6fc1-c341-11d1-b73a-444553540000} +DEFINE_GUID(DSPROPSETID_EAX_ReverbProperties, + 0x4a4e6fc1, + 0xc341, + 0x11d1, + 0xb7, 0x3a, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00); + +typedef enum +{ + DSPROPERTY_EAX_ALL, // all reverb properties + DSPROPERTY_EAX_ENVIRONMENT, // standard environment no. + DSPROPERTY_EAX_VOLUME, // loudness of the reverb + DSPROPERTY_EAX_DECAYTIME, // how long the reverb lasts + DSPROPERTY_EAX_DAMPING // the high frequencies decay faster +} DSPROPERTY_EAX_REVERBPROPERTY; + +#define EAX_NUM_STANDARD_PROPERTIES (DSPROPERTY_EAX_DAMPING + 1) + +// use this structure for get/set all properties... +typedef struct +{ + unsigned long environment; // 0 to EAX_ENVIRONMENT_COUNT-1 + float fVolume; // 0 to 1 + float fDecayTime_sec; // seconds, 0.1 to 100 + float fDamping; // 0 to 1 +} EAX_REVERBPROPERTIES; + + +enum +{ + EAX_ENVIRONMENT_GENERIC, // factory default + EAX_ENVIRONMENT_PADDEDCELL, + EAX_ENVIRONMENT_ROOM, // standard environments + EAX_ENVIRONMENT_BATHROOM, + EAX_ENVIRONMENT_LIVINGROOM, + EAX_ENVIRONMENT_STONEROOM, + EAX_ENVIRONMENT_AUDITORIUM, + EAX_ENVIRONMENT_CONCERTHALL, + EAX_ENVIRONMENT_CAVE, + EAX_ENVIRONMENT_ARENA, + EAX_ENVIRONMENT_HANGAR, + EAX_ENVIRONMENT_CARPETEDHALLWAY, + EAX_ENVIRONMENT_HALLWAY, + EAX_ENVIRONMENT_STONECORRIDOR, + EAX_ENVIRONMENT_ALLEY, + EAX_ENVIRONMENT_FOREST, + EAX_ENVIRONMENT_CITY, + EAX_ENVIRONMENT_MOUNTAINS, + EAX_ENVIRONMENT_QUARRY, + EAX_ENVIRONMENT_PLAIN, + EAX_ENVIRONMENT_PARKINGLOT, + EAX_ENVIRONMENT_SEWERPIPE, + EAX_ENVIRONMENT_UNDERWATER, + EAX_ENVIRONMENT_DRUGGED, + EAX_ENVIRONMENT_DIZZY, + EAX_ENVIRONMENT_PSYCHOTIC, + + EAX_ENVIRONMENT_COUNT // total number of environments +}; + +#define EAX_MAX_ENVIRONMENT (EAX_ENVIRONMENT_COUNT - 1) + +// presets +#define EAX_PRESET_GENERIC EAX_ENVIRONMENT_GENERIC,0.5F,1.493F,0.5F +#define EAX_PRESET_PADDEDCELL EAX_ENVIRONMENT_PADDEDCELL,0.25F,0.1F,0.0F +#define EAX_PRESET_ROOM EAX_ENVIRONMENT_ROOM,0.417F,0.4F,0.666F +#define EAX_PRESET_BATHROOM EAX_ENVIRONMENT_BATHROOM,0.653F,1.499F,0.166F +#define EAX_PRESET_LIVINGROOM EAX_ENVIRONMENT_LIVINGROOM,0.208F,0.478F,0.0F +#define EAX_PRESET_STONEROOM EAX_ENVIRONMENT_STONEROOM,0.5F,2.309F,0.888F +#define EAX_PRESET_AUDITORIUM EAX_ENVIRONMENT_AUDITORIUM,0.403F,4.279F,0.5F +#define EAX_PRESET_CONCERTHALL EAX_ENVIRONMENT_CONCERTHALL,0.5F,3.961F,0.5F +#define EAX_PRESET_CAVE EAX_ENVIRONMENT_CAVE,0.5F,2.886F,1.304F +#define EAX_PRESET_ARENA EAX_ENVIRONMENT_ARENA,0.361F,7.284F,0.332F +#define EAX_PRESET_HANGAR EAX_ENVIRONMENT_HANGAR,0.5F,10.0F,0.3F +#define EAX_PRESET_CARPETEDHALLWAY EAX_ENVIRONMENT_CARPETEDHALLWAY,0.153F,0.259F,2.0F +#define EAX_PRESET_HALLWAY EAX_ENVIRONMENT_HALLWAY,0.361F,1.493F,0.0F +#define EAX_PRESET_STONECORRIDOR EAX_ENVIRONMENT_STONECORRIDOR,0.444F,2.697F,0.638F +#define EAX_PRESET_ALLEY EAX_ENVIRONMENT_ALLEY,0.25F,1.752F,0.776F +#define EAX_PRESET_FOREST EAX_ENVIRONMENT_FOREST,0.111F,3.145F,0.472F +#define EAX_PRESET_CITY EAX_ENVIRONMENT_CITY,0.111F,2.767F,0.224F +#define EAX_PRESET_MOUNTAINS EAX_ENVIRONMENT_MOUNTAINS,0.194F,7.841F,0.472F +#define EAX_PRESET_QUARRY EAX_ENVIRONMENT_QUARRY,1.0F,1.499F,0.5F +#define EAX_PRESET_PLAIN EAX_ENVIRONMENT_PLAIN,0.097F,2.767F,0.224F +#define EAX_PRESET_PARKINGLOT EAX_ENVIRONMENT_PARKINGLOT,0.208F,1.652F,1.5F +#define EAX_PRESET_SEWERPIPE EAX_ENVIRONMENT_SEWERPIPE,0.652F,2.886F,0.25F +#define EAX_PRESET_UNDERWATER EAX_ENVIRONMENT_UNDERWATER,1.0F,1.499F,0.0F +#define EAX_PRESET_DRUGGED EAX_ENVIRONMENT_DRUGGED,0.875F,8.392F,1.388F +#define EAX_PRESET_DIZZY EAX_ENVIRONMENT_DIZZY,0.139F,17.234F,0.666F +#define EAX_PRESET_PSYCHOTIC EAX_ENVIRONMENT_PSYCHOTIC,0.486F,7.563F,0.806F + + +// EAX buffer reverb property set {4a4e6fc0-c341-11d1-b73a-444553540000} +DEFINE_GUID(DSPROPSETID_EAXBUFFER_ReverbProperties, + 0x4a4e6fc0, + 0xc341, + 0x11d1, + 0xb7, 0x3a, 0x44, 0x45, 0x53, 0x54, 0x00, 0x00); + +typedef enum +{ + DSPROPERTY_EAXBUFFER_ALL, // all reverb buffer properties + DSPROPERTY_EAXBUFFER_REVERBMIX // the wet source amount +} DSPROPERTY_EAXBUFFER_REVERBPROPERTY; + +// use this structure for get/set all properties... +typedef struct +{ + float fMix; // linear factor, 0.0F to 1.0F +} EAXBUFFER_REVERBPROPERTIES; + +#define EAX_REVERBMIX_USEDISTANCE -1.0F // out of normal range + // signifies the reverb engine should + // calculate it's own reverb mix value + // based on distance + +#endif // EAX_H + diff --git a/engine/audio/private/posix_stubs.h b/engine/audio/private/posix_stubs.h new file mode 100644 index 0000000..69ba41f --- /dev/null +++ b/engine/audio/private/posix_stubs.h @@ -0,0 +1,234 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Posix win32 replacements - Mocks trivial windows flow +// +//============================================================================= +#ifndef POSIX_AUDIO_STUBS_H +#define POSIX_AUDIO_STUBS_H + +#define DSBCAPS_LOCSOFTWARE 0 + +#define DSERR_BUFFERLOST 0 + +#define DSBSTATUS_BUFFERLOST 0x02 + +#define DSSPEAKER_GEOMETRY(x) (((x)>>16) & 0xFFFF) +#define DSSPEAKER_CONFIG(x) ((x) & 0xFFFF) + +#define DSSPEAKER_HEADPHONE -1 +#define DSSPEAKER_QUAD -2 +#define DSSPEAKER_5POINT1 -3 +#define DSSPEAKER_7POINT1 -4 + +#define DISP_CHANGE_SUCCESSFUL 0 + +#define HKEY_CURRENT_USER NULL +#define HKEY_LOCAL_MACHINE NULL +#define KEY_QUERY_VALUE 0 + +#define KEY_READ 0 +#define KEY_WRITE 1 +#define KEY_ALL_ACCESS ((ULONG)-1) + +#define SMTO_ABORTIFHUNG 0 + +#define JOY_RETURNX 0x01 +#define JOY_RETURNY 0x02 +#define JOY_RETURNZ 0x04 +#define JOY_RETURNR 0x08 +#define JOY_RETURNU 0x10 +#define JOY_RETURNV 0x20 + +#define JOYCAPS_HASPOV 0x01 +#define JOYCAPS_HASU 0x01 +#define JOYCAPS_HASV 0x01 +#define JOYCAPS_HASR 0x01 +#define JOYCAPS_HASZ 0x01 + +#define MMSYSERR_NODRIVER 1 +#define JOYERR_NOERROR 0 +#define JOY_RETURNCENTERED 0 +#define JOY_RETURNBUTTONS 0 +#define JOY_RETURNPOV 0 +#define JOY_POVCENTERED 0 +#define JOY_POVFORWARD 0 +#define JOY_POVRIGHT 0 +#define JOY_POVBACKWARD 0 +#define JOY_POVLEFT 0 + +#define CCHDEVICENAME 32 +#define CCHFORMNAME 32 + +typedef wchar_t BCHAR; + +typedef uint MMRESULT; +typedef uint32 *DWORD_PTR; +typedef char *LPCSTR; +typedef uint POINTL; + +#define IDLE_PRIORITY_CLASS 1 +#define HIGH_PRIORITY_CLASS 2 + +typedef struct _devicemode { + BCHAR dmDeviceName[CCHDEVICENAME]; + WORD dmSpecVersion; + WORD dmDriverVersion; + WORD dmSize; + WORD dmDriverExtra; + DWORD dmFields; + union u1 { + struct s { + short dmOrientation; + short dmPaperSize; + short dmPaperLength; + short dmPaperWidth; + short dmScale; + short dmCopies; + short dmDefaultSource; + short dmPrintQuality; + }; + POINTL dmPosition; + DWORD dmDisplayOrientation; + DWORD dmDisplayFixedOutput; + }; + short dmColor; + short dmDuplex; + short dmYResolution; + short dmTTOption; + short dmCollate; + BYTE dmFormName[CCHFORMNAME]; + WORD dmLogPixels; + DWORD dmBitsPerPel; + DWORD dmPelsWidth; + DWORD dmPelsHeight; + union u2 { + DWORD dmDisplayFlags; + DWORD dmNup; + }; + DWORD dmDisplayFrequency; + DWORD dmICMMethod; + DWORD dmICMIntent; + DWORD dmMediaType; + DWORD dmDitherType; + DWORD dmReserved1; + DWORD dmReserved2; + DWORD dmPanningWidth; + DWORD dmPanningHeight; +} DEVMODE, *LPDEVMODE; + +typedef uint32 MCIERROR; +typedef uint MCIDEVICEID; + +typedef struct { + DWORD_PTR dwCallback; +} MCI_GENERIC_PARMS; + +typedef struct { + DWORD_PTR dwCallback; + DWORD dwReturn; + DWORD dwItem; + DWORD dwTrack; +} MCI_STATUS_PARMS; + +typedef struct { + DWORD_PTR dwCallback; + DWORD dwFrom; + DWORD dwTo; +} MCI_PLAY_PARMS; + +typedef struct { + DWORD_PTR dwCallback; + MCIDEVICEID wDeviceID; + LPCSTR lpstrDeviceType; + LPCSTR lpstrElementName; + LPCSTR lpstrAlias; +} MCI_OPEN_PARMS; + +typedef struct { + DWORD_PTR dwCallback; + DWORD dwTimeFormat; + DWORD dwAudio; +} MCI_SET_PARMS; + +#define MCI_MAKE_TMSF(t, m, s, f) ((DWORD)(((BYTE)(t) | ((WORD)(m) << 8)) | ((DWORD)(BYTE)(s) | ((WORD)(f)<<8)) << 16)) +#define MCI_MSF_MINUTE(msf) ((BYTE)(msf)) +#define MCI_MSF_SECOND(msf) ((BYTE)(((WORD)(msf)) >> 8)) + +#define MCI_OPEN 0 +#define MCI_OPEN_TYPE 0 +#define MCI_OPEN_SHAREABLE 0 +#define MCI_FORMAT_TMSF 0 +#define MCI_SET_TIME_FORMAT 0 +#define MCI_CLOSE 0 +#define MCI_STOP 0 +#define MCI_PAUSE 0 +#define MCI_PLAY 0 +#define MCI_SET 0 +#define MCI_SET_DOOR_OPEN 0 +#define MCI_SET_DOOR_CLOSED 0 +#define MCI_STATUS_READY 0 +#define MCI_STATUS 0 +#define MCI_STATUS_ITEM 0 +#define MCI_STATUS_WAIT 0 +#define MCI_STATUS_NUMBER_OF_TRACKS 0 +#define MCI_CDA_STATUS_TYPE_TRACK 0 +#define MCI_TRACK 0 +#define MCI_WAIT 0 +#define MCI_CDA_TRACK_AUDIO 0 +#define MCI_STATUS_LENGTH 0 +#define MCI_NOTIFY 0 +#define MCI_FROM 0 +#define MCI_TO 0 +#define MCIERR_DRIVER -1 + +#define DSERR_ALLOCATED 0 + +#pragma pack(push, 1) +typedef struct tWAVEFORMATEX +{ + WORD wFormatTag; + WORD nChannels; + DWORD nSamplesPerSec; + DWORD nAvgBytesPerSec; + WORD nBlockAlign; + WORD wBitsPerSample; + WORD cbSize; +} WAVEFORMATEX, *PWAVEFORMATEX, *NPWAVEFORMATEX, *LPWAVEFORMATEX; + +typedef const WAVEFORMATEX *LPCWAVEFORMATEX; + + +typedef struct waveformat_tag +{ + WORD wFormatTag; + WORD nChannels; + DWORD nSamplesPerSec; + DWORD nAvgBytesPerSec; + WORD nBlockAlign; +} WAVEFORMAT, *PWAVEFORMAT, *NPWAVEFORMAT, *LPWAVEFORMAT; + +typedef const WAVEFORMAT *LPCWAVEFORMAT; + +typedef struct pcmwaveformat_tag +{ + WAVEFORMAT wf; + WORD wBitsPerSample; +} PCMWAVEFORMAT, *PPCMWAVEFORMAT, *NPPCMWAVEFORMAT, *LPPCMWAVEFORMAT; + +typedef const PCMWAVEFORMAT *LPCPCMWAVEFORMAT; + +typedef struct adpcmcoef_tag { + short iCoef1; + short iCoef2; +} ADPCMCOEFSET; + +typedef struct adpcmwaveformat_tag { + WAVEFORMATEX wfx; + WORD wSamplesPerBlock; + WORD wNumCoef; + ADPCMCOEFSET aCoef[1]; +} ADPCMWAVEFORMAT; + +#pragma pack(pop) +#endif + diff --git a/engine/audio/private/snd_channels.h b/engine/audio/private/snd_channels.h new file mode 100644 index 0000000..06fd065 --- /dev/null +++ b/engine/audio/private/snd_channels.h @@ -0,0 +1,213 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=============================================================================// + +#ifndef SND_CHANNELS_H +#define SND_CHANNELS_H + +#include "mathlib/vector.h" + +#if defined( _WIN32 ) +#pragma once +#endif + +class CSfxTable; +class CAudioMixer; +typedef int SoundSource; + +// DO NOT REORDER: indices to fvolume arrays in channel_t + +#define IFRONT_LEFT 0 // NOTE: must correspond to order of fvolume array below! +#define IFRONT_RIGHT 1 +#define IREAR_LEFT 2 +#define IREAR_RIGHT 3 +#define IFRONT_CENTER 4 +#define IFRONT_CENTER0 5 // dummy slot - center channel is mono, but mixers reference volume[1] slot + +#define IFRONT_LEFTD 6 // start of doppler right array +#define IFRONT_RIGHTD 7 +#define IREAR_LEFTD 8 +#define IREAR_RIGHTD 9 +#define IFRONT_CENTERD 10 +#define IFRONT_CENTERD0 11 // dummy slot - center channel is mono, but mixers reference volume[1] slot + +#define CCHANVOLUMES 12 + +//----------------------------------------------------------------------------- +// Purpose: Each currently playing wave is stored in a channel +//----------------------------------------------------------------------------- +// NOTE: 128bytes. These are memset to zero at some points. Do not add virtuals without changing that pattern. +// UNDONE: now 300 bytes... +struct channel_t +{ + int guid; // incremented each time a channel is allocated (to match with channel free in tools, etc.) + int userdata; // user specified data for syncing to tools + + CSfxTable *sfx; // the actual sound + CAudioMixer *pMixer; // The sound's instance data for this channel + + // speaker channel volumes, indexed using IFRONT_LEFT to IFRONT_CENTER. + // NOTE: never access these fvolume[] elements directly! Use channel helpers in snd_dma.cpp. + + float fvolume[CCHANVOLUMES]; // 0.0-255.0 current output volumes + float fvolume_target[CCHANVOLUMES]; // 0.0-255.0 target output volumes + float fvolume_inc[CCHANVOLUMES]; // volume increment, per frame, moves volume[i] to vol_target[i] (per spatialization) + uint nFreeChannelAtSampleTime; + + SoundSource soundsource; // see iclientsound.h for description. + int entchannel; // sound channel (CHAN_STREAM, CHAN_VOICE, etc.) + int speakerentity; // if a sound is being played through a speaker entity (e.g., on a monitor,), this is the + // entity upon which to show the lips moving, if the sound has sentence data + short master_vol; // 0-255 master volume + short basePitch; // base pitch percent (100% is normal pitch playback) + float pitch; // real-time pitch after any modulation or shift by dynamic data + int mixgroups[8]; // sound belongs to these mixgroups: world, actor, player weapon, explosion etc. + int last_mixgroupid;// last mixgroupid selected + float last_vol; // last volume after spatialization + + Vector origin; // origin of sound effect + Vector direction; // direction of the sound + float dist_mult; // distance multiplier (attenuation/clipK) + + + float dspmix; // 0 - 1.0 proportion of dsp to mix with original sound, based on distance + float dspface; // -1.0 - 1.0 (1.0 = facing listener) + float distmix; // 0 - 1.0 proportion based on distance from listner (1.0 - 100% wav right - far) + float dsp_mix_min; // for dspmix calculation - set by current preset in SND_GetDspMix + float dsp_mix_max; // for dspmix calculation - set by current preset in SND_GetDspMix + + float radius; // Radius of this sound effect (spatialization is different within the radius) + + float ob_gain; // gain drop if sound source obscured from listener + float ob_gain_target; // target gain while crossfading between ob_gain & ob_gain_target + float ob_gain_inc; // crossfade increment + + short activeIndex; + char wavtype; // 0 default, CHAR_DOPPLER, CHAR_DIRECTIONAL, CHAR_DISTVARIANT + char pad; + + char sample_prev[8]; // last sample(s) in previous input data buffer - space for 2, 16 bit, stereo samples + + int initialStreamPosition; + + int special_dsp; + + union + { + unsigned int flagsword; + struct + { + bool bUpdatePositions : 1; // if true, assume sound source can move and update according to entity + bool isSentence : 1; // true if playing linked sentence + bool bdry : 1; // if true, bypass all dsp processing for this sound (ie: music) + bool bSpeaker : 1; // true if sound is playing through in-game speaker entity. + bool bstereowav : 1; // if true, a stereo .wav file is the sample data source + + bool delayed_start : 1; // If true, sound had a delay and so same sound on same channel won't channel steal from it + bool fromserver : 1; // for snd_show, networked sounds get colored differently than local sounds + + bool bfirstpass : 1; // true if this is first time sound is spatialized + bool bTraced : 1; // true if channel was already checked this frame for obscuring + bool bfast_pitch : 1; // true if using low quality pitch (fast, but no interpolation) + + bool m_bIsFreeingChannel : 1; // true when inside S_FreeChannel - prevents reentrance + bool m_bCompatibilityAttenuation : 1; // True when we want to use goldsrc compatibility mode for the attenuation + // In that case, dist_mul is set to a relatively meaningful value in StartDynamic/StartStaticSound, + // but we interpret it totally differently in SND_GetGain. + bool m_bShouldPause : 1; // if true, sound should pause when the game is paused + bool m_bIgnorePhonemes : 1; // if true, we don't want to drive animation w/ phoneme data + } flags; + }; +}; + + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +#define MAX_CHANNELS 128 +#define MAX_DYNAMIC_CHANNELS 64 + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- + +extern channel_t channels[MAX_CHANNELS]; +// 0 to MAX_DYNAMIC_CHANNELS-1 = normal entity sounds +// MAX_DYNAMIC_CHANNELS to total_channels = static sounds + +extern int total_channels; + +class CChannelList +{ +public: + int Count(); + int GetChannelIndex( int listIndex ); + channel_t *GetChannel( int listIndex ); + void RemoveChannelFromList( int listIndex ); + bool IsQuashed( int listIndex ); + + int m_count; + short m_list[MAX_CHANNELS]; + bool m_quashed[MAX_CHANNELS]; // if true, the channel should be advanced, but not mixed, because it's been heuristically suppressed + + CUtlVector< int > m_nSpecialDSPs; + + bool m_hasSpeakerChannels : 1; + bool m_hasDryChannels : 1; + bool m_has11kChannels : 1; + bool m_has22kChannels : 1; + bool m_has44kChannels : 1; +}; + +inline int CChannelList::Count() +{ + return m_count; +} + +inline int CChannelList::GetChannelIndex( int listIndex ) +{ + return m_list[listIndex]; +} +inline channel_t *CChannelList::GetChannel( int listIndex ) +{ + return &channels[GetChannelIndex(listIndex)]; +} + +inline bool CChannelList::IsQuashed( int listIndex ) +{ + return m_quashed[listIndex]; +} + +inline void CChannelList::RemoveChannelFromList( int listIndex ) +{ + // decrease the count by one, and swap the deleted channel with + // the last one. + m_count--; + if ( m_count > 0 && listIndex != m_count ) + { + m_list[listIndex] = m_list[m_count]; + m_quashed[listIndex] = m_quashed[m_count]; + } +} + +class CActiveChannels +{ +public: + void Add( channel_t *pChannel ); + void Remove( channel_t *pChannel ); + + void GetActiveChannels( CChannelList &list ); + + void Init(); + int GetActiveCount() { return m_count; } +private: + int m_count; + short m_list[MAX_CHANNELS]; +}; + +extern CActiveChannels g_ActiveChannels; + +//============================================================================= + +#endif // SND_CHANNELS_H diff --git a/engine/audio/private/snd_convars.h b/engine/audio/private/snd_convars.h new file mode 100644 index 0000000..9e7c7b6 --- /dev/null +++ b/engine/audio/private/snd_convars.h @@ -0,0 +1,21 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_CONVARS_H +#define SND_CONVARS_H + +#if defined( _WIN32 ) +#pragma once +#endif + +#include "convar.h" + +extern ConVar snd_legacy_surround; +extern ConVar snd_surround; +extern ConVar snd_mix_async; + +#endif // SND_CONVARS_H diff --git a/engine/audio/private/snd_dev_common.cpp b/engine/audio/private/snd_dev_common.cpp new file mode 100644 index 0000000..bbfe2f1 --- /dev/null +++ b/engine/audio/private/snd_dev_common.cpp @@ -0,0 +1,623 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Device Common Base Class. +// +//=====================================================================================// + +#include "audio_pch.h" + +#define ISPEAKER_RIGHT_FRONT 0 +#define ISPEAKER_LEFT_FRONT 1 +#define ISPEAKER_RIGHT_REAR 2 +#define ISPEAKER_LEFT_REAR 3 +#define ISPEAKER_CENTER_FRONT 4 + +extern Vector listener_right; + +extern void DEBUG_StartSoundMeasure(int type, int samplecount ); +extern void DEBUG_StopSoundMeasure(int type, int samplecount ); +extern bool MIX_ScaleChannelVolume( paintbuffer_t *pPaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); + +inline bool FVolumeFrontNonZero( int *pvol ) +{ + return (pvol[IFRONT_RIGHT] || pvol[IFRONT_LEFT]); +} + +inline bool FVolumeRearNonZero( int *pvol ) +{ + return (pvol[IREAR_RIGHT] || pvol[IREAR_LEFT]); +} + +inline bool FVolumeCenterNonZero( int *pvol ) +{ + return (pvol[IFRONT_CENTER] != 0); +} + +// fade speaker volumes to mono, based on xfade value. +// ie: xfade 1.0 is full mono. +// ispeaker is speaker index, cspeaker is total # of speakers +// fmix2channels causes mono mix for 4 channel mix to mix down to 2 channels +// this is used for the 2 speaker outpu case, which uses recombined 4 channel front/rear mixing + +static float XfadeSpeakerVolToMono( float scale, float xfade, float ispeaker, float cspeaker, bool fmix2channels ) +{ + float scale_out; + float scale_target; + + if (cspeaker == 4 ) + { + // mono sound distribution: + float scale_targets[] = {0.9, 0.9, 0.9, 0.9}; // RF, LF, RR, LR + float scale_targets2ch[] = {0.9, 0.9, 0.0, 0.0}; // RF, LF, RR, LR + + if ( fmix2channels ) + scale_target = scale_targets2ch[clamp(FastFloatToSmallInt(ispeaker), 0, 3)]; + else + scale_target = scale_targets[clamp(FastFloatToSmallInt(ispeaker), 0, 3)]; + + goto XfadeExit; + } + + if (cspeaker == 5 ) + { + // mono sound distribution: + float scale_targets[] = {0.9, 0.9, 0.5, 0.5, 0.9}; // RF, LF, RR, LR, FC + scale_target = scale_targets[(int)clamp(FastFloatToSmallInt(ispeaker), 0, 4)]; + goto XfadeExit; + } + + // if (cspeaker == 2 ) + scale_target = 0.9; // front 2 speakers in stereo each get 50% of total volume in mono case + +XfadeExit: + scale_out = scale + (scale_target - scale) * xfade; + return scale_out; +} + +// given: +// 2d yaw angle to sound source (0-360), where 0 is listener_right +// pitch angle to source +// angle to speaker position (0-360), where 0 is listener_right +// speaker index +// speaker total count, +// return: scale from 0-1.0 for speaker volume. +// NOTE: as pitch angle goes to +/- 90, sound goes to mono, all speakers. + +#define PITCH_ANGLE_THRESHOLD 45.0 +#define REAR_VOL_DROP 0.5 +#define VOLCURVEPOWER 1.5 // 1.0 is a linear crossfade of volume between speakers. + // 1.5 provides a smoother, nonlinear volume transition - this is done + // because a volume of 255 played in a single speaker is + // percieved as louder than 128 + 128 in two speakers + // separated by at least 45 degrees. The nonlinear curve + // gives the volume boost needed. + +static float GetSpeakerVol( float yaw_source, float pitch_source, float mono, float yaw_speaker, int ispeaker, int cspeaker, bool fmix2channels ) +{ + float adif = fabs(yaw_source - yaw_speaker); + float pitch_angle = pitch_source; + float scale = 0.0; + float xfade = 0.0; + + if ( adif > 180 ) + adif = 360 - adif; + + // mono goes from 0.0 to 1.0 as listener moves into 'mono' radius of sound source. + // Also, as pitch_angle to sound source approaches 90 (sound above/below listener), sounds become mono. + + // convert pitch angle to 0-90 absolute pitch + if ( pitch_angle < 0) + pitch_angle += 360; + + if ( pitch_angle > 180) + pitch_angle = 360 - pitch_angle; + + if ( pitch_angle > 90) + pitch_angle = 90 - (pitch_angle - 90); + + // calculate additional mono crossfade due to pitch angle + if ( pitch_angle > PITCH_ANGLE_THRESHOLD ) + { + xfade = ( pitch_angle - PITCH_ANGLE_THRESHOLD ) / ( 90.0 - PITCH_ANGLE_THRESHOLD ); // 0.0 -> 1.0 as angle 45->90 + + mono += xfade; + mono = clamp(mono, 0.0f, 1.0f); + } + + if ( cspeaker == 2 ) + { + // 2 speaker (headphone) mix: speakers opposing, at 0 & 180 degrees + + scale = (1.0 - powf(adif/180.0, VOLCURVEPOWER)); + + goto GetVolExit; + } + + if ( adif >= 90.0 ) + goto GetVolExit; // 0.0 scale + + if ( cspeaker == 4 ) + { + // 4 ch surround: all speakers on 90 degree angles, + // scale ranges from 0.0 (at 90 degree difference between source and speaker) + // to 1.0 (0 degree difference between source and speaker) + + scale = (1.0 - powf(adif/90.0, VOLCURVEPOWER)); + + goto GetVolExit; + } + + // 5 ch surround: + + // rear speakers are on 90 degree angles and return 0.0->1.0 range over +/- 90 degrees each + // center speaker is on 45 degree angle to left/right front speaker + // center speaker has 0.0->1.0 range over 45 degrees + + switch (ispeaker) + { + default: + case ISPEAKER_RIGHT_REAR: + case ISPEAKER_LEFT_REAR: + { + // rear speakers get +/- 90 degrees of linear scaling... + scale = (1.0 - powf(adif/90.0, VOLCURVEPOWER)); + break; + } + + case ISPEAKER_CENTER_FRONT: + { + // center speaker gets +/- 45 degrees of linear scaling... + if (adif > 45.0) + goto GetVolExit; // 0.0 scale + + scale = (1.0 - powf(adif/45.0, VOLCURVEPOWER)); + break; + } + case ISPEAKER_RIGHT_FRONT: + { + if (yaw_source > yaw_speaker) + { + // if sound source is between right front speaker and center speaker, + // apply scaling over 75 degrees... + + if (adif > 75.0) + goto GetVolExit; // 0.0 scale + + scale = (1.0 - powf(adif/75.0, VOLCURVEPOWER)); + } +/* + if (yaw_source > yaw_speaker && yaw_source < (yaw_speaker + 90.0)) + { + // if sound source is between right front speaker and center speaker, + // apply scaling over 45 degrees... + if (adif > 45.0) + goto GetVolExit; // 0.0 scale + + scale = (1.0 - powf(adif/45.0, VOLCURVEPOWER)); + } +*/ + else + { + // sound source is CW from right speaker, apply scaling over 90 degrees... + scale = (1.0 - powf(adif/90.0, VOLCURVEPOWER)); + } + + break; + } + + case ISPEAKER_LEFT_FRONT: + { + if (yaw_source < yaw_speaker ) + { + // if sound source is between left front speaker and center speaker, + // apply scaling over 75 degrees... + + if (adif > 75.0) + goto GetVolExit; // 0.0 scale + + scale = (1.0 - powf(adif/75.0, VOLCURVEPOWER)); + + } +/* + if (yaw_source < yaw_speaker && yaw_source > (yaw_speaker - 90.0)) + { + // if sound source is between left front speaker and center speaker, + // apply scaling over 45 degrees... + if (adif > 45.0) + goto GetVolExit; // 0.0 scale + + scale = (1.0 - powf(adif/45.0, VOLCURVEPOWER)); + + } +*/ + else + { + // sound source is CW from right speaker, apply scaling over 90 degrees... + scale = (1.0 - powf(adif/90.0, VOLCURVEPOWER)); + } + break; + } + } + +GetVolExit: + Assert(mono <= 1.0 && mono >= 0.0); + Assert(scale <= 1.0 && scale >= 0.0); + + // crossfade speaker volumes towards mono with increased pitch angle of sound source + + scale = XfadeSpeakerVolToMono( scale, mono, ispeaker, cspeaker, fmix2channels ); + + Assert(scale <= 1.0 && scale >= 0.0); + + return scale; +} + +// given unit vector from listener to sound source, +// determine proportion of volume for sound in FL, FC, FR, RL, RR quadrants +// Scale this proportion by the distance scalar 'gain' +// If sound has 'mono' radius, blend sound to mono over 50% of radius. +void CAudioDeviceBase::SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) +{ + VPROF("CAudioDeviceBase::SpatializeChannel"); + float rfscale, rrscale, lfscale, lrscale, fcscale; + + fcscale = rfscale = lfscale = rrscale = lrscale = 0.0; + + // clear volumes + + for (int i = 0; i < CCHANVOLUMES/2; i++) + volume[i] = 0; + + // linear crossfader for 2, 4 or 5 speakers, using polar coord. separation angle as linear basis + + // get pitch & yaw angle from listener origin to sound source + + QAngle angles; + float pitch; + float source_yaw; + float yaw; + + VectorAngles(sourceDir, angles); + + pitch = angles[PITCH]; + source_yaw = angles[YAW]; + + // get 2d listener yaw angle from listener right + + QAngle angles2d; + Vector source2d; + float listener_yaw; + + source2d.x = listener_right.x; + source2d.y = listener_right.y; + source2d.z = 0.0; + + VectorNormalize(source2d); + + // convert right vector to euler angles (yaw & pitch) + + VectorAngles(source2d, angles2d); + + listener_yaw = angles2d[YAW]; + + // get yaw of sound source, with listener_yaw as reference 0. + + yaw = source_yaw - listener_yaw; + + if (yaw < 0) + yaw += 360; + + if ( !m_bSurround ) + { + // 2 ch stereo mixing + + if ( m_bHeadphone ) + { + // headphone mix: (NO HRTF) + + rfscale = GetSpeakerVol( yaw, pitch, mono, 0.0, ISPEAKER_RIGHT_FRONT, 2, false); + lfscale = GetSpeakerVol( yaw, pitch, mono, 180.0, ISPEAKER_LEFT_FRONT, 2, false ); + } + else + { + // stereo speakers at 45 & 135 degrees: (mono sounds mix down to 2 channels) + + rfscale = GetSpeakerVol( yaw, pitch, mono, 45.0, ISPEAKER_RIGHT_FRONT, 4, true ); + lfscale = GetSpeakerVol( yaw, pitch, mono, 135.0, ISPEAKER_LEFT_FRONT, 4, true ); + rrscale = GetSpeakerVol( yaw, pitch, mono, 315.0, ISPEAKER_RIGHT_REAR, 4, true ); + lrscale = GetSpeakerVol( yaw, pitch, mono, 225.0, ISPEAKER_LEFT_REAR, 4, true ); + + // add sounds coming from rear (quieter) + + rfscale = clamp((rfscale + rrscale * 0.75), 0.0, 1.0); + lfscale = clamp((lfscale + lrscale * 0.75), 0.0, 1.0); + + rrscale = 0; + lrscale = 0; + + //DevMsg("lfscale=%f rfscale=%f lrscale=%f rrscale=%f\n",lfscale,rfscale,lrscale,rrscale); + //DevMsg("pitch=%f yaw=%f \n",pitch, yaw); + } + goto SpatialExit; + } + + if ( m_bSurround && !m_bSurroundCenter ) + { + // 4 ch surround + + // linearly scale with radial distance from asource to FR, FL, RR, RL + // where FR = 45 degrees, FL = 135, RR = 315 (-45), RL = 225 (-135) + + rfscale = GetSpeakerVol( yaw, pitch, mono, 45.0, ISPEAKER_RIGHT_FRONT, 4, false ); + lfscale = GetSpeakerVol( yaw, pitch, mono, 135.0, ISPEAKER_LEFT_FRONT, 4, false ); + rrscale = GetSpeakerVol( yaw, pitch, mono, 315.0, ISPEAKER_RIGHT_REAR, 4, false ); + lrscale = GetSpeakerVol( yaw, pitch, mono, 225.0, ISPEAKER_LEFT_REAR, 4, false ); + + // DevMsg("lfscale=%f rfscale=%f lrscale=%f rrscale=%f\n",lfscale,rfscale,lrscale,rrscale); + // DevMsg("pitch=%f yaw=%f \n",pitch, yaw); + + goto SpatialExit; + } + + if ( m_bSurround && m_bSurroundCenter ) + { + // 5 ch surround + + // linearly scale with radial distance from asource to FR, FC, FL, RR, RL + // where FR = 45 degrees, FC = 90, FL = 135, RR = 315 (-45), RL = 225 (-135) + + rfscale = GetSpeakerVol( yaw, pitch, mono, 45.0, ISPEAKER_RIGHT_FRONT, 5, false ); + fcscale = GetSpeakerVol( yaw, pitch, mono, 90.0, ISPEAKER_CENTER_FRONT, 5, false ); + lfscale = GetSpeakerVol( yaw, pitch, mono, 135.0, ISPEAKER_LEFT_FRONT, 5, false ); + rrscale = GetSpeakerVol( yaw, pitch, mono, 315.0, ISPEAKER_RIGHT_REAR, 5, false ); + lrscale = GetSpeakerVol( yaw, pitch, mono, 225.0, ISPEAKER_LEFT_REAR, 5, false ); + + //DevMsg("lfscale=%f center= %f rfscale=%f lrscale=%f rrscale=%f\n",lfscale,fcscale, rfscale,lrscale,rrscale); + //DevMsg("pitch=%f yaw=%f \n",pitch, yaw); + + goto SpatialExit; + } + +SpatialExit: + + // scale volumes in each quadrant by distance attenuation. + + // volumes are 0-255: + // gain is 0.0->1.0, rscale is 0.0->1.0, so scale is 0.0->1.0 + // master_vol is 0->255, so rightvol is 0->255 + + volume[IFRONT_RIGHT] = (int) (master_vol * gain * rfscale); + volume[IFRONT_LEFT] = (int) (master_vol * gain * lfscale); + + volume[IFRONT_RIGHT] = clamp( volume[IFRONT_RIGHT], 0, 255 ); + volume[IFRONT_LEFT] = clamp( volume[IFRONT_LEFT], 0, 255 ); + + if ( m_bSurround ) + { + volume[IREAR_RIGHT] = (int) (master_vol * gain * rrscale); + volume[IREAR_LEFT] = (int) (master_vol * gain * lrscale); + + volume[IREAR_RIGHT] = clamp( volume[IREAR_RIGHT], 0, 255 ); + volume[IREAR_LEFT] = clamp( volume[IREAR_LEFT], 0, 255 ); + + if ( m_bSurroundCenter ) + { + volume[IFRONT_CENTER] = (int) (master_vol * gain * fcscale); + volume[IFRONT_CENTER0] = 0.0; + + volume[IFRONT_CENTER] = clamp( volume[IFRONT_CENTER], 0, 255); + } + } +} + +void CAudioDeviceBase::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount) +{ + VPROF("CAudioDeviceBase::ApplyDSPEffects"); + DEBUG_StartSoundMeasure( 1, samplecount ); + + DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount ); + + DEBUG_StopSoundMeasure( 1, samplecount ); +} + +void CAudioDeviceBase::MixBegin( int sampleCount ) +{ + MIX_ClearAllPaintBuffers( sampleCount, false ); +} + +void CAudioDeviceBase::MixUpsample( int sampleCount, int filtertype ) +{ + paintbuffer_t *pPaint = MIX_GetCurrentPaintbufferPtr(); + int ifilter = pPaint->ifilter; + + Assert (ifilter < CPAINTFILTERS); + + S_MixBufferUpsample2x( sampleCount, pPaint->pbuf, &(pPaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + if ( pPaint->fsurround ) + { + Assert( pPaint->pbufrear ); + S_MixBufferUpsample2x( sampleCount, pPaint->pbufrear, &(pPaint->fltmemrear[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + if ( pPaint->fsurround_center ) + { + Assert( pPaint->pbufcenter ); + S_MixBufferUpsample2x( sampleCount, pPaint->pbufcenter, &(pPaint->fltmemcenter[ifilter][0]), CPAINTFILTERMEM, filtertype ); + } + } + + // make sure on next upsample pass for this paintbuffer, new filter memory is used + pPaint->ifilter++; +} + +void CAudioDeviceBase::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + + paintbuffer_t *pPaint = MIX_GetCurrentPaintbufferPtr(); + + if ( !MIX_ScaleChannelVolume( pPaint, pChannel, volume, 1) ) + return; + + if ( FVolumeFrontNonZero(volume) ) + { + Mix8MonoWavtype( pChannel, pPaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount); + } + + if ( pPaint->fsurround ) + { + if ( FVolumeRearNonZero(volume) ) + { + Assert( pPaint->pbufrear ); + Mix8MonoWavtype( pChannel, pPaint->pbufrear + outputOffset, &volume[IREAR_LEFT], (byte *)pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround_center && FVolumeCenterNonZero(volume) ) + { + Assert( pPaint->pbufcenter ); + Mix8MonoWavtype( pChannel, pPaint->pbufcenter + outputOffset, &volume[IFRONT_CENTER], (byte *)pData, inputOffset, rateScaleFix, outCount ); + } + } +} + +void CAudioDeviceBase::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + + paintbuffer_t *pPaint = MIX_GetCurrentPaintbufferPtr(); + + if ( !MIX_ScaleChannelVolume( pPaint, pChannel, volume, 2 ) ) + return; + + if ( FVolumeFrontNonZero(volume) ) + { + Mix8StereoWavtype( pChannel, pPaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround ) + { + if ( FVolumeRearNonZero(volume) ) + { + Assert( pPaint->pbufrear ); + Mix8StereoWavtype( pChannel, pPaint->pbufrear + outputOffset, &volume[IREAR_LEFT], (byte *)pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround_center && FVolumeCenterNonZero(volume) ) + { + Assert( pPaint->pbufcenter ); + Mix8StereoWavtype( pChannel, pPaint->pbufcenter + outputOffset, &volume[IFRONT_CENTER], (byte *)pData, inputOffset, rateScaleFix, outCount ); + } + } +} + +void CAudioDeviceBase::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + + paintbuffer_t *pPaint = MIX_GetCurrentPaintbufferPtr(); + + if ( !MIX_ScaleChannelVolume( pPaint, pChannel, volume, 1 ) ) + return; + + if ( FVolumeFrontNonZero(volume) ) + { + Mix16MonoWavtype( pChannel, pPaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround ) + { + if ( FVolumeRearNonZero(volume) ) + { + Assert( pPaint->pbufrear ); + Mix16MonoWavtype( pChannel, pPaint->pbufrear + outputOffset, &volume[IREAR_LEFT], pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround_center && FVolumeCenterNonZero(volume) ) + { + Assert( pPaint->pbufcenter ); + Mix16MonoWavtype( pChannel, pPaint->pbufcenter + outputOffset, &volume[IFRONT_CENTER], pData, inputOffset, rateScaleFix, outCount ); + } + } +} + +void CAudioDeviceBase::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + + paintbuffer_t *pPaint = MIX_GetCurrentPaintbufferPtr(); + + if ( !MIX_ScaleChannelVolume( pPaint, pChannel, volume, 2 ) ) + return; + + if ( FVolumeFrontNonZero(volume) ) + { + Mix16StereoWavtype( pChannel, pPaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround ) + { + if ( FVolumeRearNonZero(volume) ) + { + Assert( pPaint->pbufrear ); + Mix16StereoWavtype( pChannel, pPaint->pbufrear + outputOffset, &volume[IREAR_LEFT], pData, inputOffset, rateScaleFix, outCount ); + } + + if ( pPaint->fsurround_center && FVolumeCenterNonZero(volume) ) + { + Assert( pPaint->pbufcenter ); + Mix16StereoWavtype( pChannel, pPaint->pbufcenter + outputOffset, &volume[IFRONT_CENTER], pData, inputOffset, rateScaleFix, outCount ); + } + } +} + +// Null Audio Device +class CAudioDeviceNull : public CAudioDeviceBase +{ +public: + bool IsActive( void ) { return false; } + bool Init( void ) { return true; } + void Shutdown( void ) {} + void Pause( void ) {} + void UnPause( void ) {} + float MixDryVolume( void ) { return 0; } + bool Should3DMix( void ) { return false; } + void StopAllSounds( void ) {} + + int PaintBegin( float, int, int ) { return 0; } + void PaintEnd( void ) {} + + void SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) {} + void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) {} + int GetOutputPosition( void ) { return 0; } + void ClearBuffer( void ) {} + void UpdateListener( const Vector&, const Vector&, const Vector&, const Vector& ) {} + + void MixBegin( int ) {} + void MixUpsample( int sampleCount, int filtertype ) {} + + void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) {} + void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) {} + void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) {} + void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) {} + + void ChannelReset( int, int, float ) {} + void TransferSamples( int end ) {} + + const char *DeviceName( void ) { return "Audio Disabled"; } + int DeviceChannels( void ) { return 2; } + int DeviceSampleBits( void ) { return 16; } + int DeviceSampleBytes( void ) { return 2; } + int DeviceDmaSpeed( void ) { return SOUND_DMA_SPEED; } + int DeviceSampleCount( void ) { return 0; } + + bool IsSurround( void ) { return false; } + bool IsSurroundCenter( void ) { return false; } + bool IsHeadphone( void ) { return false; } +}; + +IAudioDevice *Audio_GetNullDevice( void ) +{ + // singeton device here + static CAudioDeviceNull nullDevice; + return &nullDevice; +}
\ No newline at end of file diff --git a/engine/audio/private/snd_dev_common.h b/engine/audio/private/snd_dev_common.h new file mode 100644 index 0000000..45c2ee3 --- /dev/null +++ b/engine/audio/private/snd_dev_common.h @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Device Common Routines +// +//=====================================================================================// + +#ifndef SND_DEV_COMMON_H +#define SND_DEV_COMMON_H +#pragma once + +class CAudioDeviceBase : public IAudioDevice +{ +public: + virtual bool IsActive( void ) { return false; } + virtual bool Init( void ) { return false; } + virtual void Shutdown( void ) {} + virtual void Pause( void ) {} + virtual void UnPause( void ) {} + virtual float MixDryVolume( void ) { return 0; } + virtual bool Should3DMix( void ) { return m_bSurround; } + virtual void StopAllSounds( void ) {} + + virtual int PaintBegin( float, int soundtime, int paintedtime ) { return 0; } + virtual void PaintEnd( void ) {} + + virtual void SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ); + virtual void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ); + virtual int GetOutputPosition( void ) { return 0; } + virtual void ClearBuffer( void ) {} + virtual void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) {} + + virtual void MixBegin( int sampleCount ); + virtual void MixUpsample( int sampleCount, int filtertype ); + + virtual void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + virtual void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + virtual void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + virtual void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + + virtual void ChannelReset( int entnum, int channelIndex, float distanceMod ) {} + virtual void TransferSamples( int end ) {} + + virtual const char *DeviceName( void ) { return NULL; } + virtual int DeviceChannels( void ) { return 0; } + virtual int DeviceSampleBits( void ) { return 0; } + virtual int DeviceSampleBytes( void ) { return 0; } + virtual int DeviceDmaSpeed( void ) { return 1; } + virtual int DeviceSampleCount( void ) { return 0; } + + virtual bool IsSurround( void ) { return m_bSurround; } + virtual bool IsSurroundCenter( void ) { return m_bSurroundCenter; } + virtual bool IsHeadphone( void ) { return m_bHeadphone; } + + bool m_bSurround; + bool m_bSurroundCenter; + bool m_bHeadphone; +}; + +#endif // SND_DEV_COMMON_H diff --git a/engine/audio/private/snd_dev_direct.cpp b/engine/audio/private/snd_dev_direct.cpp new file mode 100644 index 0000000..9149964 --- /dev/null +++ b/engine/audio/private/snd_dev_direct.cpp @@ -0,0 +1,2094 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#include "audio_pch.h" +#include <dsound.h> +#pragma warning(disable : 4201) // nameless struct/union +#include <ks.h> +// Fix for VS 2010 build errors copied from Dota +#if !defined( NEW_DXSDK ) && ( _MSC_VER >= 1600 ) +#undef KSDATAFORMAT_SUBTYPE_WAVEFORMATEX +#undef KSDATAFORMAT_SUBTYPE_PCM +#undef KSDATAFORMAT_SUBTYPE_IEEE_FLOAT +#endif +#include <ksmedia.h> +#include "iprediction.h" +#include "eax.h" +#include "tier0/icommandline.h" +#include "video//ivideoservices.h" +#include "../../sys_dll.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool snd_firsttime; + +extern void DEBUG_StartSoundMeasure(int type, int samplecount ); +extern void DEBUG_StopSoundMeasure(int type, int samplecount ); + +// legacy support +extern ConVar sxroom_off; +extern ConVar sxroom_type; +extern ConVar sxroomwater_type; +extern float sxroom_typeprev; + +extern HWND* pmainwindow; + +typedef enum {SIS_SUCCESS, SIS_FAILURE, SIS_NOTAVAIL} sndinitstat; + +#define SECONDARY_BUFFER_SIZE 0x10000 // output buffer size in bytes +#define SECONDARY_BUFFER_SIZE_SURROUND 0x04000 // output buffer size in bytes, one per channel + +#if !defined( NEW_DXSDK ) +// hack - need to include latest dsound.h +#undef DSSPEAKER_5POINT1 +#undef DSSPEAKER_7POINT1 +#define DSSPEAKER_5POINT1 6 +#define DSSPEAKER_7POINT1 7 +#define DSSPEAKER_7POINT1_SURROUND 8 +#define DSSPEAKER_5POINT1_SURROUND 9 +#endif + +HRESULT (WINAPI *pDirectSoundCreate)(GUID FAR *lpGUID, LPDIRECTSOUND FAR *lplpDS, IUnknown FAR *pUnkOuter); + +extern void ReleaseSurround(void); +extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); +void OnSndSurroundCvarChanged( IConVar *var, const char *pOldString, float flOldValue ); +void OnSndSurroundLegacyChanged( IConVar *var, const char *pOldString, float flOldValue ); +void OnSndVarChanged( IConVar *var, const char *pOldString, float flOldValue ); + +static LPDIRECTSOUND pDS = NULL; +static LPDIRECTSOUNDBUFFER pDSBuf = NULL, pDSPBuf = NULL; + +static GUID IID_IDirectSound3DBufferDef = {0x279AFA86, 0x4981, 0x11CE, {0xA5, 0x21, 0x00, 0x20, 0xAF, 0x0B, 0xE5, 0x60}}; +static ConVar windows_speaker_config("windows_speaker_config", "-1", FCVAR_ARCHIVE); +static DWORD g_ForcedSpeakerConfig = 0; + +extern ConVar snd_mute_losefocus; + +//----------------------------------------------------------------------------- +// Purpose: Implementation of direct sound +//----------------------------------------------------------------------------- +class CAudioDirectSound : public CAudioDeviceBase +{ +public: + ~CAudioDirectSound( void ); + bool IsActive( void ) { return true; } + bool Init( void ); + void Shutdown( void ); + void Pause( void ); + void UnPause( void ); + float MixDryVolume( void ); + bool Should3DMix( void ); + void StopAllSounds( void ); + + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + void PaintEnd( void ); + + int GetOutputPosition( void ); + void ClearBuffer( void ); + void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ); + + void ChannelReset( int entnum, int channelIndex, float distanceMod ); + void TransferSamples( int end ); + + const char *DeviceName( void ); + int DeviceChannels( void ) { return m_deviceChannels; } + int DeviceSampleBits( void ) { return m_deviceSampleBits; } + int DeviceSampleBytes( void ) { return m_deviceSampleBits/8; } + int DeviceDmaSpeed( void ) { return m_deviceDmaSpeed; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + + bool IsInterleaved() { return m_isInterleaved; } + + // Singleton object + static CAudioDirectSound *m_pSingleton; + +private: + void DetectWindowsSpeakerSetup(); + bool LockDSBuffer( LPDIRECTSOUNDBUFFER pBuffer, DWORD **pdwWriteBuffer, DWORD *pdwSizeBuffer, const char *pBufferName, int lockFlags = 0 ); + bool IsUsingBufferPerSpeaker(); + + sndinitstat SNDDMA_InitDirect( void ); + bool SNDDMA_InitInterleaved( LPDIRECTSOUND lpDS, WAVEFORMATEX* lpFormat, int channelCount ); + bool SNDDMA_InitSurround(LPDIRECTSOUND lpDS, WAVEFORMATEX* lpFormat, DSBCAPS* lpdsbc, int cchan); + void S_TransferSurround16( portable_samplepair_t *pfront, portable_samplepair_t *prear, portable_samplepair_t *pcenter, int lpaintedtime, int endtime, int cchan); + void S_TransferSurround16Interleaved( const portable_samplepair_t *pfront, const portable_samplepair_t *prear, const portable_samplepair_t *pcenter, int lpaintedtime, int endtime); + void S_TransferSurround16Interleaved_FullLock( const portable_samplepair_t *pfront, const portable_samplepair_t *prear, const portable_samplepair_t *pcenter, int lpaintedtime, int endtime); + + int m_deviceChannels; // channels per hardware output buffer (1 for quad/5.1, 2 for stereo) + int m_deviceSampleBits; // bits per sample (16) + int m_deviceSampleCount; // count of mono samples in output buffer + int m_deviceDmaSpeed; // samples per second per output buffer + int m_bufferSizeBytes; // size of a single hardware output buffer, in bytes + + DWORD m_outputBufferStartOffset; // output buffer playback starting byte offset + HINSTANCE m_hInstDS; + bool m_isInterleaved; +}; + +CAudioDirectSound *CAudioDirectSound::m_pSingleton = NULL; + +LPDIRECTSOUNDBUFFER pDSBufFL = NULL; +LPDIRECTSOUNDBUFFER pDSBufFR = NULL; +LPDIRECTSOUNDBUFFER pDSBufRL = NULL; +LPDIRECTSOUNDBUFFER pDSBufRR = NULL; +LPDIRECTSOUNDBUFFER pDSBufFC = NULL; +LPDIRECTSOUND3DBUFFER pDSBuf3DFL = NULL; +LPDIRECTSOUND3DBUFFER pDSBuf3DFR = NULL; +LPDIRECTSOUND3DBUFFER pDSBuf3DRL = NULL; +LPDIRECTSOUND3DBUFFER pDSBuf3DRR = NULL; +LPDIRECTSOUND3DBUFFER pDSBuf3DFC = NULL; + +// ----------------------------------------------------------------------------- // +// Helpers. +// ----------------------------------------------------------------------------- // + + +CAudioDirectSound::~CAudioDirectSound( void ) +{ + m_pSingleton = NULL; +} + +bool CAudioDirectSound::Init( void ) +{ + m_hInstDS = NULL; + + static bool first = true; + if ( first ) + { + snd_surround.InstallChangeCallback( &OnSndSurroundCvarChanged ); + snd_legacy_surround.InstallChangeCallback( &OnSndSurroundLegacyChanged ); + snd_mute_losefocus.InstallChangeCallback( &OnSndVarChanged ); + first = false; + } + + if ( SNDDMA_InitDirect() == SIS_SUCCESS) + { + if ( g_pVideo != NULL ) + { + g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::SET_DIRECT_SOUND_DEVICE, pDS ); + } + + return true; + } + + return false; +} + +void CAudioDirectSound::Shutdown( void ) +{ + ReleaseSurround(); + + if (pDSBuf) + { + pDSBuf->Stop(); + pDSBuf->Release(); + } + + // only release primary buffer if it's not also the mixing buffer we just released + if (pDSPBuf && (pDSBuf != pDSPBuf)) + { + pDSPBuf->Release(); + } + + if (pDS) + { + pDS->SetCooperativeLevel(*pmainwindow, DSSCL_NORMAL); + pDS->Release(); + } + + pDS = NULL; + pDSBuf = NULL; + pDSPBuf = NULL; + + if ( m_hInstDS ) + { + FreeLibrary( m_hInstDS ); + m_hInstDS = NULL; + } + + if ( this == CAudioDirectSound::m_pSingleton ) + { + CAudioDirectSound::m_pSingleton = NULL; + } +} + +// Total number of samples that have played out to hardware +// for current output buffer (ie: from buffer offset start). +// return playback position within output playback buffer: +// the output units are dependant on the device channels +// so the ouput units for a 2 channel device are as 16 bit LR pairs +// and the output unit for a 1 channel device are as 16 bit mono samples. +// take into account the original start position within the buffer, and +// calculate difference between current position (with buffer wrap) and +// start position. +int CAudioDirectSound::GetOutputPosition( void ) +{ + int samp16; + int start, current; + DWORD dwCurrent; + + // get size in bytes of output buffer + const int size_bytes = m_bufferSizeBytes; + if ( IsUsingBufferPerSpeaker() ) + { + // mono output buffers + // get byte offset of playback cursor in Front Left output buffer + pDSBufFL->GetCurrentPosition(&dwCurrent, NULL); + + start = (int) m_outputBufferStartOffset; + current = (int) dwCurrent; + } + else + { + // multi-channel interleavd output buffer + // get byte offset of playback cursor in output buffer + pDSBuf->GetCurrentPosition(&dwCurrent, NULL); + + start = (int) m_outputBufferStartOffset; + current = (int) dwCurrent; + } + + // get 16 bit samples played, relative to buffer starting offset + if (current > start) + { + // get difference & convert to 16 bit mono samples + samp16 = (current - start) >> SAMPLE_16BIT_SHIFT; + } + else + { + // get difference (with buffer wrap) convert to 16 bit mono samples + samp16 = ((size_bytes - start) + current) >> SAMPLE_16BIT_SHIFT; + } + + int outputPosition = samp16 / DeviceChannels(); + + return outputPosition; +} + +void CAudioDirectSound::Pause( void ) +{ + if (pDSBuf) + { + pDSBuf->Stop(); + } + + if ( pDSBufFL ) pDSBufFL->Stop(); + if ( pDSBufFR ) pDSBufFR->Stop(); + if ( pDSBufRL ) pDSBufRL->Stop(); + if ( pDSBufRR ) pDSBufRR->Stop(); + if ( pDSBufFC ) pDSBufFC->Stop(); +} + + +void CAudioDirectSound::UnPause( void ) +{ + if (pDSBuf) + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + + if (pDSBufFL) pDSBufFL->Play(0, 0, DSBPLAY_LOOPING); + if (pDSBufFR) pDSBufFR->Play(0, 0, DSBPLAY_LOOPING); + if (pDSBufRL) pDSBufRL->Play(0, 0, DSBPLAY_LOOPING); + if (pDSBufRR) pDSBufRR->Play( 0, 0, DSBPLAY_LOOPING); + if (pDSBufFC) pDSBufFC->Play( 0, 0, DSBPLAY_LOOPING); +} + + +float CAudioDirectSound::MixDryVolume( void ) +{ + return 0; +} + + +bool CAudioDirectSound::Should3DMix( void ) +{ + if ( m_bSurround ) + return true; + return false; +} + + +IAudioDevice *Audio_CreateDirectSoundDevice( void ) +{ + if ( !CAudioDirectSound::m_pSingleton ) + CAudioDirectSound::m_pSingleton = new CAudioDirectSound; + + if ( CAudioDirectSound::m_pSingleton->Init() ) + { + if (snd_firsttime) + DevMsg ("DirectSound initialized\n"); + + return CAudioDirectSound::m_pSingleton; + } + + DevMsg ("DirectSound failed to init\n"); + + delete CAudioDirectSound::m_pSingleton; + CAudioDirectSound::m_pSingleton = NULL; + + return NULL; +} + +int CAudioDirectSound::PaintBegin( float mixAheadTime, int soundtime, int lpaintedtime ) +{ + // soundtime - total full samples that have been played out to hardware at dmaspeed + // paintedtime - total full samples that have been mixed at speed + // endtime - target for full samples in mixahead buffer at speed + // samps - size of output buffer in full samples + + int mixaheadtime = mixAheadTime * DeviceDmaSpeed(); + int endtime = soundtime + mixaheadtime; + + if ( endtime <= lpaintedtime ) + return endtime; + + uint nSamples = endtime - lpaintedtime; + if ( nSamples & 0x3 ) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + nSamples += (4 - (nSamples & 3)); + } + // clamp to min 512 samples per mix + if ( nSamples > 0 && nSamples < 512 ) + { + nSamples = 512; + } + endtime = lpaintedtime + nSamples; + + int fullsamps = DeviceSampleCount() / DeviceChannels(); + if ( (endtime - soundtime) > fullsamps) + { + endtime = soundtime + fullsamps; + endtime += (4 - (endtime & 3)); + } + + DWORD dwStatus; + + // If using surround, there are 4 or 5 different buffers being used and the pDSBuf is NULL. + if ( IsUsingBufferPerSpeaker() ) + { + if (pDSBufFL->GetStatus(&dwStatus) != DS_OK) + Msg ("Couldn't get SURROUND FL sound buffer status\n"); + + if (dwStatus & DSBSTATUS_BUFFERLOST) + pDSBufFL->Restore(); + + if (!(dwStatus & DSBSTATUS_PLAYING)) + pDSBufFL->Play(0, 0, DSBPLAY_LOOPING); + + if (pDSBufFR->GetStatus(&dwStatus) != DS_OK) + Msg ("Couldn't get SURROUND FR sound buffer status\n"); + + if (dwStatus & DSBSTATUS_BUFFERLOST) + pDSBufFR->Restore(); + + if (!(dwStatus & DSBSTATUS_PLAYING)) + pDSBufFR->Play(0, 0, DSBPLAY_LOOPING); + + if (pDSBufRL->GetStatus(&dwStatus) != DS_OK) + Msg ("Couldn't get SURROUND RL sound buffer status\n"); + + if (dwStatus & DSBSTATUS_BUFFERLOST) + pDSBufRL->Restore(); + + if (!(dwStatus & DSBSTATUS_PLAYING)) + pDSBufRL->Play(0, 0, DSBPLAY_LOOPING); + + if (pDSBufRR->GetStatus(&dwStatus) != DS_OK) + Msg ("Couldn't get SURROUND RR sound buffer status\n"); + + if (dwStatus & DSBSTATUS_BUFFERLOST) + pDSBufRR->Restore(); + + if (!(dwStatus & DSBSTATUS_PLAYING)) + pDSBufRR->Play(0, 0, DSBPLAY_LOOPING); + + if ( m_bSurroundCenter ) + { + if (pDSBufFC->GetStatus(&dwStatus) != DS_OK) + Msg ("Couldn't get SURROUND FC sound buffer status\n"); + + if (dwStatus & DSBSTATUS_BUFFERLOST) + pDSBufFC->Restore(); + + if (!(dwStatus & DSBSTATUS_PLAYING)) + pDSBufFC->Play(0, 0, DSBPLAY_LOOPING); + } + } + else if (pDSBuf) + { + if ( pDSBuf->GetStatus (&dwStatus) != DS_OK ) + Msg("Couldn't get sound buffer status\n"); + + if ( dwStatus & DSBSTATUS_BUFFERLOST ) + pDSBuf->Restore(); + + if ( !(dwStatus & DSBSTATUS_PLAYING) ) + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + } + + return endtime; +} + + +void CAudioDirectSound::PaintEnd( void ) +{ +} + + +void CAudioDirectSound::ClearBuffer( void ) +{ + int clear; + + DWORD dwSizeFL, dwSizeFR, dwSizeRL, dwSizeRR, dwSizeFC; + char *pDataFL, *pDataFR, *pDataRL, *pDataRR, *pDataFC; + + dwSizeFC = 0; // compiler warning + pDataFC = NULL; + + if ( IsUsingBufferPerSpeaker() ) + { + int SURROUNDreps; + HRESULT SURROUNDhresult; + SURROUNDreps = 0; + + if ( !pDSBufFL && !pDSBufFR && !pDSBufRL && !pDSBufRR && !pDSBufFC ) + return; + + while ((SURROUNDhresult = pDSBufFL->Lock(0, m_bufferSizeBytes, (void**)&pDataFL, &dwSizeFL, NULL, NULL, 0)) != DS_OK) + { + if (SURROUNDhresult != DSERR_BUFFERLOST) + { + Msg ("S_ClearBuffer: DS::Lock FL Sound Buffer Failed\n"); + S_Shutdown (); + return; + } + + if (++SURROUNDreps > 10000) + { + Msg ("S_ClearBuffer: DS: couldn't restore FL buffer\n"); + S_Shutdown (); + return; + } + } + + SURROUNDreps = 0; + while ((SURROUNDhresult = pDSBufFR->Lock(0, m_bufferSizeBytes, (void**)&pDataFR, &dwSizeFR, NULL, NULL, 0)) != DS_OK) + { + if (SURROUNDhresult != DSERR_BUFFERLOST) + { + Msg ("S_ClearBuffer: DS::Lock FR Sound Buffer Failed\n"); + S_Shutdown (); + return; + } + + if (++SURROUNDreps > 10000) + { + Msg ("S_ClearBuffer: DS: couldn't restore FR buffer\n"); + S_Shutdown (); + return; + } + } + + SURROUNDreps = 0; + while ((SURROUNDhresult = pDSBufRL->Lock(0, m_bufferSizeBytes, (void**)&pDataRL, &dwSizeRL, NULL, NULL, 0)) != DS_OK) + { + if (SURROUNDhresult != DSERR_BUFFERLOST) + { + Msg ("S_ClearBuffer: DS::Lock RL Sound Buffer Failed\n"); + S_Shutdown (); + return; + } + + if (++SURROUNDreps > 10000) + { + Msg ("S_ClearBuffer: DS: couldn't restore RL buffer\n"); + S_Shutdown (); + return; + } + } + + SURROUNDreps = 0; + while ((SURROUNDhresult = pDSBufRR->Lock(0, m_bufferSizeBytes, (void**)&pDataRR, &dwSizeRR, NULL, NULL, 0)) != DS_OK) + { + if (SURROUNDhresult != DSERR_BUFFERLOST) + { + Msg ("S_ClearBuffer: DS::Lock RR Sound Buffer Failed\n"); + S_Shutdown (); + return; + } + + if (++SURROUNDreps > 10000) + { + Msg ("S_ClearBuffer: DS: couldn't restore RR buffer\n"); + S_Shutdown (); + return; + } + } + + if (m_bSurroundCenter) + { + SURROUNDreps = 0; + while ((SURROUNDhresult = pDSBufFC->Lock(0, m_bufferSizeBytes, (void**)&pDataFC, &dwSizeFC, NULL, NULL, 0)) != DS_OK) + { + if (SURROUNDhresult != DSERR_BUFFERLOST) + { + Msg ("S_ClearBuffer: DS::Lock FC Sound Buffer Failed\n"); + S_Shutdown (); + return; + } + + if (++SURROUNDreps > 10000) + { + Msg ("S_ClearBuffer: DS: couldn't restore FC buffer\n"); + S_Shutdown (); + return; + } + } + } + + Q_memset(pDataFL, 0, m_bufferSizeBytes); + Q_memset(pDataFR, 0, m_bufferSizeBytes); + Q_memset(pDataRL, 0, m_bufferSizeBytes); + Q_memset(pDataRR, 0, m_bufferSizeBytes); + + if (m_bSurroundCenter) + Q_memset(pDataFC, 0, m_bufferSizeBytes); + + pDSBufFL->Unlock(pDataFL, dwSizeFL, NULL, 0); + pDSBufFR->Unlock(pDataFR, dwSizeFR, NULL, 0); + pDSBufRL->Unlock(pDataRL, dwSizeRL, NULL, 0); + pDSBufRR->Unlock(pDataRR, dwSizeRR, NULL, 0); + + if (m_bSurroundCenter) + pDSBufFC->Unlock(pDataFC, dwSizeFC, NULL, 0); + + return; + } + + if ( !pDSBuf ) + return; + + if ( DeviceSampleBits() == 8 ) + clear = 0x80; + else + clear = 0; + + if (pDSBuf) + { + DWORD dwSize; + DWORD *pData; + int reps; + HRESULT hresult; + + reps = 0; + while ((hresult = pDSBuf->Lock(0, m_bufferSizeBytes, (void**)&pData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Msg("S_ClearBuffer: DS::Lock Sound Buffer Failed\n"); + S_Shutdown(); + return; + } + + if (++reps > 10000) + { + Msg("S_ClearBuffer: DS: couldn't restore buffer\n"); + S_Shutdown(); + return; + } + } + + Q_memset(pData, clear, dwSize); + + pDSBuf->Unlock(pData, dwSize, NULL, 0); + } +} + +void CAudioDirectSound::StopAllSounds( void ) +{ +} + +bool CAudioDirectSound::SNDDMA_InitInterleaved( LPDIRECTSOUND lpDS, WAVEFORMATEX* lpFormat, int channelCount ) +{ + WAVEFORMATEXTENSIBLE wfx = { 0 } ; // DirectSoundBuffer wave format (extensible) + + // set the channel mask and number of channels based on the command line parameter + if(channelCount == 2) + { + wfx.Format.nChannels = 2; + wfx.dwChannelMask = KSAUDIO_SPEAKER_STEREO; // SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT; + } + else if(channelCount == 4) + { + wfx.Format.nChannels = 4; + wfx.dwChannelMask = KSAUDIO_SPEAKER_QUAD; // SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; + } + else if(channelCount == 6) + { + wfx.Format.nChannels = 6; + wfx.dwChannelMask = KSAUDIO_SPEAKER_5POINT1; // SPEAKER_FRONT_LEFT | SPEAKER_FRONT_RIGHT | SPEAKER_FRONT_CENTER | SPEAKER_LOW_FREQUENCY | SPEAKER_BACK_LEFT | SPEAKER_BACK_RIGHT; + } + else + { + return false; + } + + // setup the extensible structure + wfx.Format.wFormatTag = WAVE_FORMAT_EXTENSIBLE; + //wfx.Format.nChannels = SET ABOVE + wfx.Format.nSamplesPerSec = lpFormat->nSamplesPerSec; + wfx.Format.wBitsPerSample = lpFormat->wBitsPerSample; + wfx.Format.nBlockAlign = wfx.Format.wBitsPerSample / 8 * wfx.Format.nChannels; + wfx.Format.nAvgBytesPerSec = wfx.Format.nSamplesPerSec * wfx.Format.nBlockAlign; + wfx.Format.cbSize = 22; // size from after this to end of extensible struct. sizeof(WORD + DWORD + GUID) + wfx.Samples.wValidBitsPerSample = lpFormat->wBitsPerSample; + //wfx.dwChannelMask = SET ABOVE BASED ON COMMAND LINE PARAMETERS + wfx.SubFormat = KSDATAFORMAT_SUBTYPE_PCM; + + // setup the DirectSound + DSBUFFERDESC dsbdesc = { 0 }; // DirectSoundBuffer descriptor + dsbdesc.dwSize = sizeof(DSBUFFERDESC); + dsbdesc.dwFlags = 0; + + dsbdesc.dwBufferBytes = SECONDARY_BUFFER_SIZE_SURROUND * channelCount; + + dsbdesc.lpwfxFormat = (WAVEFORMATEX*)&wfx; + bool bSuccess = false; + for ( int i = 0; i < 3; i++ ) + { + switch(i) + { + case 0: + dsbdesc.dwFlags = DSBCAPS_LOCHARDWARE; + break; + case 1: + dsbdesc.dwFlags = DSBCAPS_LOCSOFTWARE; + break; + case 2: + dsbdesc.dwFlags = 0; + break; + } + if ( !snd_mute_losefocus.GetBool() ) + { + dsbdesc.dwFlags |= DSBCAPS_GLOBALFOCUS; + } + + if(!FAILED(lpDS->CreateSoundBuffer(&dsbdesc, &pDSBuf, NULL))) + { + bSuccess = true; + break; + } + } + if ( !bSuccess ) + return false; + + DWORD dwSize = 0, dwWrite; + DWORD *pBuffer = 0; + if ( !LockDSBuffer( pDSBuf, &pBuffer, &dwSize, "DS_INTERLEAVED", DSBLOCK_ENTIREBUFFER ) ) + return false; + + m_deviceChannels = wfx.Format.nChannels; + m_deviceSampleBits = wfx.Format.wBitsPerSample; + m_deviceDmaSpeed = wfx.Format.nSamplesPerSec; + m_bufferSizeBytes = dsbdesc.dwBufferBytes; + m_isInterleaved = true; + + Q_memset( pBuffer, 0, dwSize ); + + pDSBuf->Unlock(pBuffer, dwSize, NULL, 0); + + // Make sure mixer is active (this was moved after the zeroing to avoid popping on startup -- at least when using the dx9.0b debug .dlls) + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + + pDSBuf->Stop(); + pDSBuf->GetCurrentPosition(&m_outputBufferStartOffset, &dwWrite); + + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + + return true; +} + +/* +================== +SNDDMA_InitDirect + +Direct-Sound support +================== +*/ +sndinitstat CAudioDirectSound::SNDDMA_InitDirect( void ) +{ + DSBUFFERDESC dsbuf; + DSBCAPS dsbcaps; + DWORD dwSize, dwWrite; + WAVEFORMATEX format; + WAVEFORMATEX pformat; + HRESULT hresult; + void *lpData = NULL; + bool primary_format_set = false; + int pri_channels = 2; + + if (!m_hInstDS) + { + m_hInstDS = LoadLibrary("dsound.dll"); + if (m_hInstDS == NULL) + { + Warning( "Couldn't load dsound.dll\n"); + return SIS_FAILURE; + } + + pDirectSoundCreate = (long (__stdcall *)(struct _GUID *,struct IDirectSound ** ,struct IUnknown *))GetProcAddress(m_hInstDS,"DirectSoundCreate"); + if (!pDirectSoundCreate) + { + Warning( "Couldn't get DS proc addr\n"); + return SIS_FAILURE; + } + } + + while ((hresult = pDirectSoundCreate(NULL, &pDS, NULL)) != DS_OK) + { + if (hresult != DSERR_ALLOCATED) + { + DevMsg ("DirectSound create failed\n"); + return SIS_FAILURE; + } + + return SIS_NOTAVAIL; + } + + // get snd_surround value from window settings + DetectWindowsSpeakerSetup(); + + m_bSurround = false; + m_bSurroundCenter = false; + m_bHeadphone = false; + m_isInterleaved = false; + + switch ( snd_surround.GetInt() ) + { + case 0: + m_bHeadphone = true; // stereo headphone + pri_channels = 2; // primary buffer mixes stereo input data + break; + default: + case 2: + pri_channels = 2; // primary buffer mixes stereo input data + break; // no surround + case 4: + m_bSurround = true; // quad surround + pri_channels = 1; // primary buffer mixes 3d mono input data + break; + case 5: + case 7: + m_bSurround = true; // 5.1 surround + m_bSurroundCenter = true; + pri_channels = 1; // primary buffer mixes 3d mono input data + break; + } + + m_deviceChannels = pri_channels; // secondary buffers should have same # channels as primary + m_deviceSampleBits = 16; // hardware bits per sample + m_deviceDmaSpeed = SOUND_DMA_SPEED; // hardware playback rate + + Q_memset( &format, 0, sizeof(format) ); + format.wFormatTag = WAVE_FORMAT_PCM; + format.nChannels = pri_channels; + format.wBitsPerSample = m_deviceSampleBits; + format.nSamplesPerSec = m_deviceDmaSpeed; + format.nBlockAlign = format.nChannels * format.wBitsPerSample / 8; + format.cbSize = 0; + format.nAvgBytesPerSec = format.nSamplesPerSec * format.nBlockAlign; + + DSCAPS dscaps; + Q_memset( &dscaps, 0, sizeof(dscaps) ); + dscaps.dwSize = sizeof(dscaps); + if (DS_OK != pDS->GetCaps(&dscaps)) + { + Warning( "Couldn't get DS caps\n"); + } + + if (dscaps.dwFlags & DSCAPS_EMULDRIVER) + { + Warning( "No DirectSound driver installed\n"); + Shutdown(); + return SIS_FAILURE; + } + + if (DS_OK != pDS->SetCooperativeLevel(*pmainwindow, DSSCL_EXCLUSIVE)) + { + Warning( "Set coop level failed\n"); + Shutdown(); + return SIS_FAILURE; + } + + // get access to the primary buffer, if possible, so we can set the + // sound hardware format + Q_memset( &dsbuf, 0, sizeof(dsbuf) ); + dsbuf.dwSize = sizeof(DSBUFFERDESC); + dsbuf.dwFlags = DSBCAPS_PRIMARYBUFFER; + if ( snd_legacy_surround.GetBool() || m_bSurround ) + { + dsbuf.dwFlags |= DSBCAPS_CTRL3D; + } + dsbuf.dwBufferBytes = 0; + dsbuf.lpwfxFormat = NULL; + + Q_memset( &dsbcaps, 0, sizeof(dsbcaps) ); + dsbcaps.dwSize = sizeof(dsbcaps); + + if ( !CommandLine()->CheckParm("-snoforceformat")) + { + if (DS_OK == pDS->CreateSoundBuffer(&dsbuf, &pDSPBuf, NULL)) + { + pformat = format; + + if (DS_OK != pDSPBuf->SetFormat(&pformat)) + { + if (snd_firsttime) + DevMsg ("Set primary sound buffer format: no\n"); + } + else + { + if (snd_firsttime) + DevMsg ("Set primary sound buffer format: yes\n"); + + primary_format_set = true; + } + } + } + + if ( m_bSurround ) + { + // try to init surround + m_bSurround = false; + if ( snd_legacy_surround.GetBool() ) + { + if (snd_surround.GetInt() == 4) + { + // attempt to init 4 channel surround + m_bSurround = SNDDMA_InitSurround(pDS, &format, &dsbcaps, 4); + } + else if (snd_surround.GetInt() == 5 || snd_surround.GetInt() == 7) + { + // attempt to init 5 channel surround + m_bSurroundCenter = SNDDMA_InitSurround(pDS, &format, &dsbcaps, 5); + m_bSurround = m_bSurroundCenter; + } + } + if ( !m_bSurround ) + { + pri_channels = 6; + if ( snd_surround.GetInt() < 5 ) + { + pri_channels = 4; + } + + m_bSurround = SNDDMA_InitInterleaved( pDS, &format, pri_channels ); + } + } + + if ( !m_bSurround ) + { + // snd_surround.SetValue( 0 ); + if ( !primary_format_set || !CommandLine()->CheckParm ("-primarysound") ) + { + // create the secondary buffer we'll actually work with + Q_memset( &dsbuf, 0, sizeof(dsbuf) ); + dsbuf.dwSize = sizeof(DSBUFFERDESC); + dsbuf.dwFlags = DSBCAPS_LOCSOFTWARE; // NOTE: don't use CTRLFREQUENCY (slow) + dsbuf.dwBufferBytes = SECONDARY_BUFFER_SIZE; + dsbuf.lpwfxFormat = &format; + if ( !snd_mute_losefocus.GetBool() ) + { + dsbuf.dwFlags |= DSBCAPS_GLOBALFOCUS; + } + + if (DS_OK != pDS->CreateSoundBuffer(&dsbuf, &pDSBuf, NULL)) + { + Warning( "DS:CreateSoundBuffer Failed"); + Shutdown(); + return SIS_FAILURE; + } + + m_deviceChannels = format.nChannels; + m_deviceSampleBits = format.wBitsPerSample; + m_deviceDmaSpeed = format.nSamplesPerSec; + + Q_memset(&dsbcaps, 0, sizeof(dsbcaps)); + dsbcaps.dwSize = sizeof(dsbcaps); + + if (DS_OK != pDSBuf->GetCaps( &dsbcaps )) + { + Warning( "DS:GetCaps failed\n"); + Shutdown(); + return SIS_FAILURE; + } + + if ( snd_firsttime ) + DevMsg ("Using secondary sound buffer\n"); + } + else + { + if (DS_OK != pDS->SetCooperativeLevel(*pmainwindow, DSSCL_WRITEPRIMARY)) + { + Warning( "Set coop level failed\n"); + Shutdown(); + return SIS_FAILURE; + } + + Q_memset(&dsbcaps, 0, sizeof(dsbcaps)); + dsbcaps.dwSize = sizeof(dsbcaps); + if (DS_OK != pDSPBuf->GetCaps(&dsbcaps)) + { + Msg ("DS:GetCaps failed\n"); + return SIS_FAILURE; + } + + pDSBuf = pDSPBuf; + DevMsg ("Using primary sound buffer\n"); + } + + if ( snd_firsttime ) + { + DevMsg(" %d channel(s)\n" + " %d bits/sample\n" + " %d samples/sec\n", + DeviceChannels(), DeviceSampleBits(), DeviceDmaSpeed()); + } + + // initialize the buffer + m_bufferSizeBytes = dsbcaps.dwBufferBytes; + int reps = 0; + while ((hresult = pDSBuf->Lock(0, m_bufferSizeBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitDirect: DS::Lock Sound Buffer Failed\n"); + Shutdown(); + return SIS_FAILURE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitDirect: DS: couldn't restore buffer\n"); + Shutdown(); + return SIS_FAILURE; + } + } + + Q_memset( lpData, 0, dwSize ); + pDSBuf->Unlock(lpData, dwSize, NULL, 0); + + // Make sure mixer is active (this was moved after the zeroing to avoid popping on startup -- at least when using the dx9.0b debug .dlls) + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + + // we don't want anyone to access the buffer directly w/o locking it first. + lpData = NULL; + + pDSBuf->Stop(); + + pDSBuf->GetCurrentPosition(&m_outputBufferStartOffset, &dwWrite); + + pDSBuf->Play(0, 0, DSBPLAY_LOOPING); + } + + // number of mono samples output buffer may hold + m_deviceSampleCount = m_bufferSizeBytes/(DeviceSampleBytes()); + + return SIS_SUCCESS; + +} + +static DWORD GetSpeakerConfigForSurroundMode( int surroundMode, const char **pConfigDesc ) +{ + DWORD newSpeakerConfig = DSSPEAKER_STEREO; + const char *speakerConfigDesc = ""; + + switch ( surroundMode ) + { + case 0: + newSpeakerConfig = DSSPEAKER_HEADPHONE; + speakerConfigDesc = "headphone"; + break; + + case 2: + default: + newSpeakerConfig = DSSPEAKER_STEREO; + speakerConfigDesc = "stereo speaker"; + break; + + case 4: + newSpeakerConfig = DSSPEAKER_QUAD; + speakerConfigDesc = "quad speaker"; + break; + + case 5: + newSpeakerConfig = DSSPEAKER_5POINT1; + speakerConfigDesc = "5.1 speaker"; + break; + + case 7: + newSpeakerConfig = DSSPEAKER_7POINT1; + speakerConfigDesc = "7.1 speaker"; + break; + } + if ( pConfigDesc ) + { + *pConfigDesc = speakerConfigDesc; + } + return newSpeakerConfig; +} + +// Read the speaker config from windows +static DWORD GetWindowsSpeakerConfig() +{ + DWORD speaker_config = windows_speaker_config.GetInt(); + if ( windows_speaker_config.GetInt() < 0 ) + { + speaker_config = DSSPEAKER_STEREO; + if (DS_OK == pDS->GetSpeakerConfig( &speaker_config )) + { + // split out settings + speaker_config = DSSPEAKER_CONFIG(speaker_config); + if ( speaker_config == DSSPEAKER_7POINT1_SURROUND ) + speaker_config = DSSPEAKER_7POINT1; + if ( speaker_config == DSSPEAKER_5POINT1_SURROUND) + speaker_config = DSSPEAKER_5POINT1; + } + windows_speaker_config.SetValue((int)speaker_config); + } + + return speaker_config; +} + +// Writes snd_surround convar given a directsound speaker config +static void SetSurroundModeFromSpeakerConfig( DWORD speakerConfig ) +{ + // set the cvar to be the windows setting + switch (speakerConfig) + { + case DSSPEAKER_HEADPHONE: + snd_surround.SetValue(0); + break; + + case DSSPEAKER_MONO: + case DSSPEAKER_STEREO: + default: + snd_surround.SetValue( 2 ); + break; + + case DSSPEAKER_QUAD: + snd_surround.SetValue(4); + break; + + case DSSPEAKER_5POINT1: + snd_surround.SetValue(5); + break; + + case DSSPEAKER_7POINT1: + snd_surround.SetValue(7); + break; + } +} +/* + Sets the snd_surround_speakers cvar based on the windows setting +*/ + +void CAudioDirectSound::DetectWindowsSpeakerSetup() +{ + // detect speaker settings from windows + DWORD speaker_config = GetWindowsSpeakerConfig(); + SetSurroundModeFromSpeakerConfig(speaker_config); + + // DEBUG + if (speaker_config == DSSPEAKER_MONO) + DevMsg( "DS:mono configuration detected\n"); + + if (speaker_config == DSSPEAKER_HEADPHONE) + DevMsg( "DS:headphone configuration detected\n"); + + if (speaker_config == DSSPEAKER_STEREO) + DevMsg( "DS:stereo speaker configuration detected\n"); + + if (speaker_config == DSSPEAKER_QUAD) + DevMsg( "DS:quad speaker configuration detected\n"); + + if (speaker_config == DSSPEAKER_SURROUND) + DevMsg( "DS:surround speaker configuration detected\n"); + + if (speaker_config == DSSPEAKER_5POINT1) + DevMsg( "DS:5.1 speaker configuration detected\n"); + + if (speaker_config == DSSPEAKER_7POINT1) + DevMsg( "DS:7.1 speaker configuration detected\n"); +} + +/* + Updates windows settings based on snd_surround_speakers cvar changing + This should only happen if the user has changed it via the console or the UI + Changes won't take effect until the engine has restarted +*/ +void OnSndSurroundCvarChanged( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + // if the old value is -1, we're setting this from the detect routine for the first time + // no need to reset the device + if (!pDS || flOldValue == -1 ) + return; + + // get the user's previous speaker config + DWORD speaker_config = GetWindowsSpeakerConfig(); + + // get the new config + DWORD newSpeakerConfig = 0; + const char *speakerConfigDesc = ""; + + ConVarRef var( pVar ); + newSpeakerConfig = GetSpeakerConfigForSurroundMode( var.GetInt(), &speakerConfigDesc ); + // make sure the config has changed + if (newSpeakerConfig == speaker_config) + return; + + // set new configuration + windows_speaker_config.SetValue( (int)newSpeakerConfig ); + + Msg("Speaker configuration has been changed to %s.\n", speakerConfigDesc); + + // restart sound system so it takes effect + g_pSoundServices->RestartSoundSystem(); +} + +void OnSndSurroundLegacyChanged( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + if ( pDS && CAudioDirectSound::m_pSingleton ) + { + ConVarRef var( pVar ); + // should either be interleaved or have legacy surround set, not both + if ( CAudioDirectSound::m_pSingleton->IsInterleaved() == var.GetBool() ) + { + Msg( "Legacy Surround %s.\n", var.GetBool() ? "enabled" : "disabled" ); + // restart sound system so it takes effect + g_pSoundServices->RestartSoundSystem(); + } + } +} + +void OnSndVarChanged( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + ConVarRef var(pVar); + // restart sound system so the change takes effect + if ( var.GetInt() != int(flOldValue) ) + { + g_pSoundServices->RestartSoundSystem(); + } +} + +/* + Release all Surround buffer pointers +*/ +void ReleaseSurround(void) +{ + if ( pDSBuf3DFL != NULL ) + { + pDSBuf3DFL->Release(); + pDSBuf3DFL = NULL; + } + + if ( pDSBuf3DFR != NULL) + { + pDSBuf3DFR->Release(); + pDSBuf3DFR = NULL; + } + + if ( pDSBuf3DRL != NULL ) + { + pDSBuf3DRL->Release(); + pDSBuf3DRL = NULL; + } + + if ( pDSBuf3DRR != NULL ) + { + pDSBuf3DRR->Release(); + pDSBuf3DRR = NULL; + } + + if ( pDSBufFL != NULL ) + { + pDSBufFL->Release(); + pDSBufFL = NULL; + } + + if ( pDSBufFR != NULL ) + { + pDSBufFR->Release(); + pDSBufFR = NULL; + } + + if ( pDSBufRL != NULL ) + { + pDSBufRL->Release(); + pDSBufRL = NULL; + } + + if ( pDSBufRR != NULL ) + { + pDSBufRR->Release(); + pDSBufRR = NULL; + } + + if ( pDSBufFC != NULL ) + { + pDSBufFC->Release(); + pDSBufFC = NULL; + } +} + +void DEBUG_DS_FillSquare( void *lpData, DWORD dwSize ) +{ + short *lpshort = (short *)lpData; + DWORD j = min((DWORD)10000, dwSize/2); + + for (DWORD i = 0; i < j; i++) + lpshort[i] = 8000; +} + +void DEBUG_DS_FillSquare2( void *lpData, DWORD dwSize ) +{ + short *lpshort = (short *)lpData; + DWORD j = min((DWORD)1000, dwSize/2); + + for (DWORD i = 0; i < j; i++) + lpshort[i] = 16000; +} + +// helper to set default buffer params +void DS3D_SetBufferParams( LPDIRECTSOUND3DBUFFER pDSBuf3D, D3DVECTOR *pbpos, D3DVECTOR *pbdir ) +{ + DS3DBUFFER bparm; + D3DVECTOR bvel; + D3DVECTOR bpos, bdir; + HRESULT hr; + + bvel.x = 0.0f; bvel.y = 0.0f; bvel.z = 0.0f; + bpos = *pbpos; + bdir = *pbdir; + + bparm.dwSize = sizeof(DS3DBUFFER); + + hr = pDSBuf3D->GetAllParameters( &bparm ); + + bparm.vPosition = bpos; + bparm.vVelocity = bvel; + bparm.dwInsideConeAngle = 5.0; // narrow cones for each speaker + bparm.dwOutsideConeAngle = 10.0; + bparm.vConeOrientation = bdir; + bparm.lConeOutsideVolume = DSBVOLUME_MIN; + bparm.flMinDistance = 100.0; // no rolloff (until > 2.0 meter distance) + bparm.flMaxDistance = DS3D_DEFAULTMAXDISTANCE; + bparm.dwMode = DS3DMODE_NORMAL; + + hr = pDSBuf3D->SetAllParameters( &bparm, DS3D_DEFERRED ); +} + +// Initialization for Surround sound support (4 channel or 5 channel). +// Creates 4 or 5 mono 3D buffers to be used as Front Left, (Front Center), Front Right, Rear Left, Rear Right +bool CAudioDirectSound::SNDDMA_InitSurround(LPDIRECTSOUND lpDS, WAVEFORMATEX* lpFormat, DSBCAPS* lpdsbc, int cchan) +{ + DSBUFFERDESC dsbuf; + WAVEFORMATEX wvex; + DWORD dwSize, dwWrite; + int reps; + HRESULT hresult; + void *lpData = NULL; + + if ( lpDS == NULL ) return FALSE; + + // Force format to mono channel + + memcpy(&wvex, lpFormat, sizeof(WAVEFORMATEX)); + wvex.nChannels = 1; + wvex.nBlockAlign = wvex.nChannels * wvex.wBitsPerSample / 8; + wvex.nAvgBytesPerSec = wvex.nSamplesPerSec * wvex.nBlockAlign; + + memset (&dsbuf, 0, sizeof(dsbuf)); + dsbuf.dwSize = sizeof(DSBUFFERDESC); + // NOTE: LOCHARDWARE causes SB AWE64 to crash in it's DSOUND driver + dsbuf.dwFlags = DSBCAPS_CTRL3D; // don't use CTRLFREQUENCY (slow) + if ( !snd_mute_losefocus.GetBool() ) + { + dsbuf.dwFlags |= DSBCAPS_GLOBALFOCUS; + } + + // reserve space for each buffer + + dsbuf.dwBufferBytes = SECONDARY_BUFFER_SIZE_SURROUND; + + dsbuf.lpwfxFormat = &wvex; + + // create 4 mono buffers FL, FR, RL, RR + + if (DS_OK != lpDS->CreateSoundBuffer(&dsbuf, &pDSBufFL, NULL)) + { + Warning( "DS:CreateSoundBuffer for 3d front left failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != lpDS->CreateSoundBuffer(&dsbuf, &pDSBufFR, NULL)) + { + Warning( "DS:CreateSoundBuffer for 3d front right failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != lpDS->CreateSoundBuffer(&dsbuf, &pDSBufRL, NULL)) + { + Warning( "DS:CreateSoundBuffer for 3d rear left failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != lpDS->CreateSoundBuffer(&dsbuf, &pDSBufRR, NULL)) + { + Warning( "DS:CreateSoundBuffer for 3d rear right failed"); + ReleaseSurround(); + return FALSE; + } + + // create center channel + + if (cchan == 5) + { + if (DS_OK != lpDS->CreateSoundBuffer(&dsbuf, &pDSBufFC, NULL)) + { + Warning( "DS:CreateSoundBuffer for 3d front center failed"); + ReleaseSurround(); + return FALSE; + } + } + + // Try to get 4 or 5 3D buffers from the mono DS buffers + + if (DS_OK != pDSBufFL->QueryInterface(IID_IDirectSound3DBufferDef, (void**)&pDSBuf3DFL)) + { + Warning( "DS:Query 3DBuffer for 3d front left failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != pDSBufFR->QueryInterface(IID_IDirectSound3DBufferDef, (void**)&pDSBuf3DFR)) + { + Warning( "DS:Query 3DBuffer for 3d front right failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != pDSBufRL->QueryInterface(IID_IDirectSound3DBufferDef, (void**)&pDSBuf3DRL)) + { + Warning( "DS:Query 3DBuffer for 3d rear left failed"); + ReleaseSurround(); + return FALSE; + } + + if (DS_OK != pDSBufRR->QueryInterface(IID_IDirectSound3DBufferDef, (void**)&pDSBuf3DRR)) + { + Warning( "DS:Query 3DBuffer for 3d rear right failed"); + ReleaseSurround(); + return FALSE; + } + + if (cchan == 5) + { + if (DS_OK != pDSBufFC->QueryInterface(IID_IDirectSound3DBufferDef, (void**)&pDSBuf3DFC)) + { + Warning( "DS:Query 3DBuffer for 3d front center failed"); + ReleaseSurround(); + return FALSE; + } + } + + // set listener position & orientation. + // DS uses left handed coord system: +x is right, +y is up, +z is forward + + HRESULT hr; + + IDirectSound3DListener *plistener = NULL; + + hr = pDSPBuf->QueryInterface(IID_IDirectSound3DListener, (void**)&plistener); + if (plistener) + { + DS3DLISTENER lparm; + lparm.dwSize = sizeof(DS3DLISTENER); + + hr = plistener->GetAllParameters( &lparm ); + + hr = plistener->SetOrientation( 0.0f,0.0f,1.0f, 0.0f,1.0f,0.0f, DS3D_IMMEDIATE); // frontx,y,z topx,y,z + hr = plistener->SetPosition(0.0f, 0.0f, 0.0f, DS3D_IMMEDIATE); + } + else + { + Warning( "DS: failed to get 3D listener interface."); + ReleaseSurround(); + return FALSE; + } + + // set 3d buffer position and orientation params + + D3DVECTOR bpos, bdir; + + bpos.x = -1.0; bpos.y = 0.0; bpos.z = 1.0; // FL + bdir.x = 1.0; bdir.y = 0.0; bdir.z = -1.0; + + DS3D_SetBufferParams( pDSBuf3DFL, &bpos, &bdir ); + + bpos.x = 1.0; bpos.y = 0.0; bpos.z = 1.0; // FR + bdir.x = -1.0; bdir.y = 0.0; bdir.z = -1.0; + + DS3D_SetBufferParams( pDSBuf3DFR, &bpos, &bdir ); + + bpos.x = -1.0; bpos.y = 0.0; bpos.z = -1.0; // RL + bdir.x = 1.0; bdir.y = 0.0; bdir.z = 1.0; + + DS3D_SetBufferParams( pDSBuf3DRL, &bpos, &bdir ); + + bpos.x = 1.0; bpos.y = 0.0; bpos.z = -1.0; // RR + bdir.x = -1.0; bdir.y = 0.0; bdir.z = 1.0; + + DS3D_SetBufferParams( pDSBuf3DRR, &bpos, &bdir ); + + if (cchan == 5) + { + bpos.x = 0.0; bpos.y = 0.0; bpos.z = 1.0; // FC + bdir.x = 0.0; bdir.y = 0.0; bdir.z = -1.0; + + DS3D_SetBufferParams( pDSBuf3DFC, &bpos, &bdir ); + } + + // commit all buffer param settings + + hr = plistener->CommitDeferredSettings(); + + m_deviceChannels = 1; // 1 mono 3d output buffer + m_deviceSampleBits = lpFormat->wBitsPerSample; + m_deviceDmaSpeed = lpFormat->nSamplesPerSec; + + memset(lpdsbc, 0, sizeof(DSBCAPS)); + lpdsbc->dwSize = sizeof(DSBCAPS); + + if (DS_OK != pDSBufFL->GetCaps (lpdsbc)) + { + Warning( "DS:GetCaps failed for 3d sound buffer\n"); + ReleaseSurround(); + return FALSE; + } + + pDSBufFL->Play(0, 0, DSBPLAY_LOOPING); + pDSBufFR->Play(0, 0, DSBPLAY_LOOPING); + pDSBufRL->Play(0, 0, DSBPLAY_LOOPING); + pDSBufRR->Play(0, 0, DSBPLAY_LOOPING); + + if (cchan == 5) + pDSBufFC->Play(0, 0, DSBPLAY_LOOPING); + + if (snd_firsttime) + DevMsg(" %d channel(s)\n" + " %d bits/sample\n" + " %d samples/sec\n", + cchan, DeviceSampleBits(), DeviceDmaSpeed()); + + m_bufferSizeBytes = lpdsbc->dwBufferBytes; + + // Test everything just like in the normal initialization. + if (cchan == 5) + { + reps = 0; + while ((hresult = pDSBufFC->Lock(0, lpdsbc->dwBufferBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitDirect: DS::Lock Sound Buffer Failed for FC\n"); + ReleaseSurround(); + return FALSE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitDirect: DS: couldn't restore buffer for FC\n"); + ReleaseSurround(); + return FALSE; + } + } + memset(lpData, 0, dwSize); +// DEBUG_DS_FillSquare( lpData, dwSize ); + pDSBufFC->Unlock(lpData, dwSize, NULL, 0); + } + + reps = 0; + while ((hresult = pDSBufFL->Lock(0, lpdsbc->dwBufferBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitSurround: DS::Lock Sound Buffer Failed for 3d FL\n"); + ReleaseSurround(); + return FALSE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitSurround: DS: couldn't restore buffer for 3d FL\n"); + ReleaseSurround(); + return FALSE; + } + } + memset(lpData, 0, dwSize); +// DEBUG_DS_FillSquare( lpData, dwSize ); + pDSBufFL->Unlock(lpData, dwSize, NULL, 0); + + reps = 0; + while ((hresult = pDSBufFR->Lock(0, lpdsbc->dwBufferBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitSurround: DS::Lock Sound Buffer Failed for 3d FR\n"); + ReleaseSurround(); + return FALSE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitSurround: DS: couldn't restore buffer for FR\n"); + ReleaseSurround(); + return FALSE; + } + } + memset(lpData, 0, dwSize); +// DEBUG_DS_FillSquare( lpData, dwSize ); + pDSBufFR->Unlock(lpData, dwSize, NULL, 0); + + reps = 0; + while ((hresult = pDSBufRL->Lock(0, lpdsbc->dwBufferBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitDirect: DS::Lock Sound Buffer Failed for RL\n"); + ReleaseSurround(); + return FALSE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitDirect: DS: couldn't restore buffer for RL\n"); + ReleaseSurround(); + return FALSE; + } + } + memset(lpData, 0, dwSize); +// DEBUG_DS_FillSquare( lpData, dwSize ); + pDSBufRL->Unlock(lpData, dwSize, NULL, 0); + + reps = 0; + while ((hresult = pDSBufRR->Lock(0, lpdsbc->dwBufferBytes, (void**)&lpData, &dwSize, NULL, NULL, 0)) != DS_OK) + { + if (hresult != DSERR_BUFFERLOST) + { + Warning( "SNDDMA_InitDirect: DS::Lock Sound Buffer Failed for RR\n"); + ReleaseSurround(); + return FALSE; + } + + if (++reps > 10000) + { + Warning( "SNDDMA_InitDirect: DS: couldn't restore buffer for RR\n"); + ReleaseSurround(); + return FALSE; + } + } + memset(lpData, 0, dwSize); +// DEBUG_DS_FillSquare( lpData, dwSize ); + pDSBufRR->Unlock(lpData, dwSize, NULL, 0); + + lpData = NULL; // this is invalid now + + // OK Stop and get our positions and were good to go. + pDSBufFL->Stop(); + pDSBufFR->Stop(); + pDSBufRL->Stop(); + pDSBufRR->Stop(); + if (cchan == 5) + pDSBufFC->Stop(); + + // get hardware playback position, store it, syncronize all buffers to FL + + pDSBufFL->GetCurrentPosition(&m_outputBufferStartOffset, &dwWrite); + pDSBufFR->SetCurrentPosition(m_outputBufferStartOffset); + pDSBufRL->SetCurrentPosition(m_outputBufferStartOffset); + pDSBufRR->SetCurrentPosition(m_outputBufferStartOffset); + if (cchan == 5) + pDSBufFC->SetCurrentPosition(m_outputBufferStartOffset); + + pDSBufFL->Play(0, 0, DSBPLAY_LOOPING); + pDSBufFR->Play(0, 0, DSBPLAY_LOOPING); + pDSBufRL->Play(0, 0, DSBPLAY_LOOPING); + pDSBufRR->Play(0, 0, DSBPLAY_LOOPING); + if (cchan == 5) + pDSBufFC->Play(0, 0, DSBPLAY_LOOPING); + + if (snd_firsttime) + Warning( "3d surround sound initialization successful\n"); + + return TRUE; +} + +void CAudioDirectSound::UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) +{ +} + +void CAudioDirectSound::ChannelReset( int entnum, int channelIndex, float distanceMod ) +{ +} + +const char *CAudioDirectSound::DeviceName( void ) +{ + if ( m_bSurroundCenter ) + return "5 Channel Surround"; + + if ( m_bSurround ) + return "4 Channel Surround"; + + return "Direct Sound"; +} + +// use the partial buffer locking code in stereo as well - not available when recording a movie +ConVar snd_lockpartial("snd_lockpartial","1"); + +// Transfer up to a full paintbuffer (PAINTBUFFER_SIZE) of stereo samples +// out to the directsound secondary buffer(s). +// For 4 or 5 ch surround, there are 4 or 5 mono 16 bit secondary DS streaming buffers. +// For stereo speakers, there is one stereo 16 bit secondary DS streaming buffer. + +void CAudioDirectSound::TransferSamples( int end ) +{ + int lpaintedtime = g_paintedtime; + int endtime = end; + + // When Surround is enabled, divert to 4 or 5 chan xfer scheme. + if ( m_bSurround ) + { + if ( m_isInterleaved ) + { + S_TransferSurround16Interleaved( PAINTBUFFER, REARPAINTBUFFER, CENTERPAINTBUFFER, lpaintedtime, endtime); + } + else + { + int cchan = ( m_bSurroundCenter ? 5 : 4); + + S_TransferSurround16( PAINTBUFFER, REARPAINTBUFFER, CENTERPAINTBUFFER, lpaintedtime, endtime, cchan); + } + return; + } + else if ( snd_lockpartial.GetBool() && DeviceChannels() == 2 && DeviceSampleBits() == 16 && !SND_IsRecording() ) + { + S_TransferSurround16Interleaved( PAINTBUFFER, NULL, NULL, lpaintedtime, endtime ); + } + else + { + DWORD *pBuffer = NULL; + DWORD dwSize = 0; + if ( !LockDSBuffer( pDSBuf, &pBuffer, &dwSize, "DS_STEREO" ) ) + { + S_Shutdown(); + S_Startup(); + return; + } + if ( pBuffer ) + { + if ( DeviceChannels() == 2 && DeviceSampleBits() == 16 ) + { + S_TransferStereo16( pBuffer, PAINTBUFFER, lpaintedtime, endtime ); + } + else + { + // UNDONE: obsolete - no 8 bit mono output supported + S_TransferPaintBuffer( pBuffer, PAINTBUFFER, lpaintedtime, endtime ); + } + pDSBuf->Unlock( pBuffer, dwSize, NULL, 0 ); + } + } +} + +bool CAudioDirectSound::IsUsingBufferPerSpeaker() +{ + return m_bSurround && !m_isInterleaved; +} + +bool CAudioDirectSound::LockDSBuffer( LPDIRECTSOUNDBUFFER pBuffer, DWORD **pdwWriteBuffer, DWORD *pdwSizeBuffer, const char *pBufferName, int lockFlags ) +{ + if ( !pBuffer ) + return false; + HRESULT hr; + int reps = 0; + while ((hr = pBuffer->Lock(0, m_bufferSizeBytes, (void**)pdwWriteBuffer, pdwSizeBuffer, + NULL, NULL, lockFlags)) != DS_OK) + { + if (hr != DSERR_BUFFERLOST) + { + Msg ("DS::Lock Sound Buffer Failed %s\n", pBufferName); + return false; + } + + if (++reps > 10000) + { + Msg ("DS:: couldn't restore buffer %s\n", pBufferName); + return false; + } + } + return true; +} + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Given front, rear and center stereo paintbuffers, split samples into 4 or 5 mono directsound buffers (FL, FC, FR, RL, RR) +void CAudioDirectSound::S_TransferSurround16( portable_samplepair_t *pfront, portable_samplepair_t *prear, portable_samplepair_t *pcenter, int lpaintedtime, int endtime, int cchan) +{ + int lpos; + DWORD *pdwWriteFL=NULL, *pdwWriteFR=NULL, *pdwWriteRL=NULL, *pdwWriteRR=NULL, *pdwWriteFC=NULL; + DWORD dwSizeFL=0, dwSizeFR=0, dwSizeRL=0, dwSizeRR=0, dwSizeFC=0; + int i, j, *snd_p, *snd_rp, *snd_cp, volumeFactor; + short *snd_out_fleft, *snd_out_fright, *snd_out_rleft, *snd_out_rright, *snd_out_fcenter; + + pdwWriteFC = NULL; // compiler warning + dwSizeFC = 0; + snd_out_fcenter = NULL; + + volumeFactor = S_GetMasterVolume() * 256; + + // lock all 4 or 5 mono directsound buffers FL, FR, RL, RR, FC + if ( !LockDSBuffer( pDSBufFL, &pdwWriteFL, &dwSizeFL, "FL" ) || + !LockDSBuffer( pDSBufFR, &pdwWriteFR, &dwSizeFR, "FR" ) || + !LockDSBuffer( pDSBufRL, &pdwWriteRL, &dwSizeRL, "RL" ) || + !LockDSBuffer( pDSBufRR, &pdwWriteRR, &dwSizeRR, "RR" ) ) + { + S_Shutdown(); + S_Startup(); + return; + } + + if (cchan == 5 && !LockDSBuffer( pDSBufFC, &pdwWriteFC, &dwSizeFC, "FC" )) + { + S_Shutdown (); + S_Startup (); + return; + } + + // take stereo front and rear paintbuffers, and center paintbuffer if provided, + // and copy samples into the 4 or 5 mono directsound buffers + + snd_rp = (int *)prear; + snd_cp = (int *)pcenter; + snd_p = (int *)pfront; + + int linearCount; // space in output buffer for linearCount mono samples + int sampleMonoCount = DeviceSampleCount(); // number of mono samples per output buffer (was;(DeviceSampleCount()>>1)) + int sampleMask = sampleMonoCount - 1; + + // paintedtime - number of full samples that have played since start + // endtime - number of full samples to play to - endtime is g_soundtime + mixahead samples + + while (lpaintedtime < endtime) + { + lpos = lpaintedtime & sampleMask; // lpos is next output position in output buffer + + linearCount = sampleMonoCount - lpos; + + // limit output count to requested number of samples + + if (linearCount > endtime - lpaintedtime) + linearCount = endtime - lpaintedtime; + + snd_out_fleft = (short *)pdwWriteFL + lpos; + snd_out_fright = (short *)pdwWriteFR + lpos; + snd_out_rleft = (short *)pdwWriteRL + lpos; + snd_out_rright = (short *)pdwWriteRR + lpos; + + if (cchan == 5) + snd_out_fcenter = (short *)pdwWriteFC + lpos; + + // for 16 bit sample in the front and rear stereo paintbuffers, copy + // into the 4 or 5 FR, FL, RL, RR, FC directsound paintbuffers + + for (i=0, j= 0 ; i<linearCount ; i++, j+=2) + { + snd_out_fleft[i] = (snd_p[j]*volumeFactor)>>8; + snd_out_fright[i] = (snd_p[j + 1]*volumeFactor)>>8; + snd_out_rleft[i] = (snd_rp[j]*volumeFactor)>>8; + snd_out_rright[i] = (snd_rp[j + 1]*volumeFactor)>>8; + } + + // copy front center buffer (mono) data to center chan directsound paintbuffer + + if (cchan == 5) + { + for (i=0, j=0 ; i<linearCount ; i++, j+=2) + { + snd_out_fcenter[i] = (snd_cp[j]*volumeFactor)>>8; + + } + } + + snd_p += linearCount << 1; + snd_rp += linearCount << 1; + snd_cp += linearCount << 1; + + lpaintedtime += linearCount; + } + + pDSBufFL->Unlock(pdwWriteFL, dwSizeFL, NULL, 0); + pDSBufFR->Unlock(pdwWriteFR, dwSizeFR, NULL, 0); + pDSBufRL->Unlock(pdwWriteRL, dwSizeRL, NULL, 0); + pDSBufRR->Unlock(pdwWriteRR, dwSizeRR, NULL, 0); + + if (cchan == 5) + pDSBufFC->Unlock(pdwWriteFC, dwSizeFC, NULL, 0); +} + +struct surround_transfer_t +{ + int paintedtime; + int linearCount; + int sampleMask; + int channelCount; + int *snd_p; + int *snd_rp; + int *snd_cp; + short *pOutput; +}; + +static void TransferSamplesToSurroundBuffer( int outputCount, surround_transfer_t &transfer ) +{ + int i, j; + int volumeFactor = S_GetMasterVolume() * 256; + + if ( transfer.channelCount == 2 ) + { + for (i=0, j=0; i<outputCount ; i++, j+=2) + { + transfer.pOutput[0] = (transfer.snd_p[j]*volumeFactor)>>8; // FL + transfer.pOutput[1] = (transfer.snd_p[j + 1]*volumeFactor)>>8; // FR + transfer.pOutput += 2; + } + } + // no center channel, 4 channel surround + else if ( transfer.channelCount == 4 ) + { + for (i=0, j=0; i<outputCount ; i++, j+=2) + { + transfer.pOutput[0] = (transfer.snd_p[j]*volumeFactor)>>8; // FL + transfer.pOutput[1] = (transfer.snd_p[j + 1]*volumeFactor)>>8; // FR + transfer.pOutput[2] = (transfer.snd_rp[j]*volumeFactor)>>8; // RL + transfer.pOutput[3] = (transfer.snd_rp[j + 1]*volumeFactor)>>8; // RR + transfer.pOutput += 4; + //Assert( baseOffset <= (DeviceSampleCount()) ); + } + } + else + { + Assert(transfer.snd_cp); + // 6 channel / 5.1 + for (i=0, j=0 ; i<outputCount ; i++, j+=2) + { + transfer.pOutput[0] = (transfer.snd_p[j]*volumeFactor)>>8; // FL + transfer.pOutput[1] = (transfer.snd_p[j + 1]*volumeFactor)>>8; // FR + + transfer.pOutput[2] = (transfer.snd_cp[j]*volumeFactor)>>8; // Center + + transfer.pOutput[3] = 0; + + transfer.pOutput[4] = (transfer.snd_rp[j]*volumeFactor)>>8; // RL + transfer.pOutput[5] = (transfer.snd_rp[j + 1]*volumeFactor)>>8; // RR + +#if 0 + // average channels into the subwoofer, let the sub filter the output + // NOTE: avg l/r rear to do 2 shifts instead of divide by 5 + int sumFront = (int)transfer.pOutput[0] + (int)transfer.pOutput[1] + (int)transfer.pOutput[2]; + int sumRear = (int)transfer.pOutput[4] + (int)transfer.pOutput[5]; + transfer.pOutput[3] = (sumFront + (sumRear>>1)) >> 2; +#endif + + transfer.pOutput += 6; + //Assert( baseOffset <= (DeviceSampleCount()) ); + } + } + + transfer.snd_p += outputCount << 1; + if ( transfer.snd_rp ) + { + transfer.snd_rp += outputCount << 1; + } + if ( transfer.snd_cp ) + { + transfer.snd_cp += outputCount << 1; + } + + transfer.paintedtime += outputCount; + transfer.linearCount -= outputCount; + +} + +void CAudioDirectSound::S_TransferSurround16Interleaved_FullLock( const portable_samplepair_t *pfront, const portable_samplepair_t *prear, const portable_samplepair_t *pcenter, int lpaintedtime, int endtime ) +{ + int lpos; + DWORD *pdwWrite = NULL; + DWORD dwSize = 0; + int i, j, *snd_p, *snd_rp, *snd_cp, volumeFactor; + + volumeFactor = S_GetMasterVolume() * 256; + int channelCount = m_bSurroundCenter ? 5 : 4; + if ( DeviceChannels() == 2 ) + { + channelCount = 2; + } + + // lock single interleaved buffer + if ( !LockDSBuffer( pDSBuf, &pdwWrite, &dwSize, "DS_INTERLEAVED" ) ) + { + S_Shutdown (); + S_Startup (); + return; + } + + // take stereo front and rear paintbuffers, and center paintbuffer if provided, + // and copy samples into the 4 or 5 mono directsound buffers + + snd_rp = (int *)prear; + snd_cp = (int *)pcenter; + snd_p = (int *)pfront; + + int linearCount; // space in output buffer for linearCount mono samples + int sampleMonoCount = m_bufferSizeBytes/(DeviceSampleBytes()*DeviceChannels()); // number of mono samples per output buffer (was;(DeviceSampleCount()>>1)) + int sampleMask = sampleMonoCount - 1; + + // paintedtime - number of full samples that have played since start + // endtime - number of full samples to play to - endtime is g_soundtime + mixahead samples + + short *pOutput = (short *)pdwWrite; + while (lpaintedtime < endtime) + { + lpos = lpaintedtime & sampleMask; // lpos is next output position in output buffer + + linearCount = sampleMonoCount - lpos; + + // limit output count to requested number of samples + + if (linearCount > endtime - lpaintedtime) + linearCount = endtime - lpaintedtime; + + if ( channelCount == 4 ) + { + int baseOffset = lpos * channelCount; + for (i=0, j= 0 ; i<linearCount ; i++, j+=2) + { + pOutput[baseOffset+0] = (snd_p[j]*volumeFactor)>>8; // FL + pOutput[baseOffset+1] = (snd_p[j + 1]*volumeFactor)>>8; // FR + pOutput[baseOffset+2] = (snd_rp[j]*volumeFactor)>>8; // RL + pOutput[baseOffset+3] = (snd_rp[j + 1]*volumeFactor)>>8; // RR + baseOffset += 4; + } + } + else + { + Assert(channelCount==5); // 6 channel / 5.1 + int baseOffset = lpos * 6; + for (i=0, j= 0 ; i<linearCount ; i++, j+=2) + { + pOutput[baseOffset+0] = (snd_p[j]*volumeFactor)>>8; // FL + pOutput[baseOffset+1] = (snd_p[j + 1]*volumeFactor)>>8; // FR + + pOutput[baseOffset+2] = (snd_cp[j]*volumeFactor)>>8; // Center + // NOTE: Let the hardware mix the sub from the main channels since + // we don't have any sub-specific sounds, or direct sub-addressing + pOutput[baseOffset+3] = 0; + + pOutput[baseOffset+4] = (snd_rp[j]*volumeFactor)>>8; // RL + pOutput[baseOffset+5] = (snd_rp[j + 1]*volumeFactor)>>8; // RR + + + baseOffset += 6; + } + } + + snd_p += linearCount << 1; + snd_rp += linearCount << 1; + snd_cp += linearCount << 1; + + lpaintedtime += linearCount; + } + + pDSBuf->Unlock(pdwWrite, dwSize, NULL, 0); +} + +void CAudioDirectSound::S_TransferSurround16Interleaved( const portable_samplepair_t *pfront, const portable_samplepair_t *prear, const portable_samplepair_t *pcenter, int lpaintedtime, int endtime ) +{ + if ( !pDSBuf ) + return; + if ( !snd_lockpartial.GetBool() ) + { + S_TransferSurround16Interleaved_FullLock( pfront, prear, pcenter, lpaintedtime, endtime ); + return; + } + // take stereo front and rear paintbuffers, and center paintbuffer if provided, + // and copy samples into the 4 or 5 mono directsound buffers + + surround_transfer_t transfer; + transfer.snd_rp = (int *)prear; + transfer.snd_cp = (int *)pcenter; + transfer.snd_p = (int *)pfront; + + int sampleMonoCount = DeviceSampleCount()/DeviceChannels(); // number of full samples per output buffer + Assert(IsPowerOfTwo(sampleMonoCount)); + transfer.sampleMask = sampleMonoCount - 1; + transfer.paintedtime = lpaintedtime; + transfer.linearCount = endtime - lpaintedtime; + // paintedtime - number of full samples that have played since start + // endtime - number of full samples to play to - endtime is g_soundtime + mixahead samples + int channelCount = m_bSurroundCenter ? 6 : 4; + if ( DeviceChannels() == 2 ) + { + channelCount = 2; + } + transfer.channelCount = channelCount; + void *pBuffer0=NULL; + void *pBuffer1=NULL; + DWORD size0, size1; + int lpos = transfer.paintedtime & transfer.sampleMask; // lpos is next output position in output buffer + + int offset = lpos*2*channelCount; + int lockSize = transfer.linearCount*2*channelCount; + int reps = 0; + HRESULT hr; + while ( (hr = pDSBuf->Lock( offset, lockSize, &pBuffer0, &size0, &pBuffer1, &size1, 0 )) != DS_OK ) + { + if ( hr == DSERR_BUFFERLOST ) + { + if ( ++reps < 10000 ) + continue; + } + Msg ("DS::Lock Sound Buffer Failed\n"); + return; + } + + if ( pBuffer0 ) + { + transfer.pOutput = (short *)pBuffer0; + TransferSamplesToSurroundBuffer( size0 / (channelCount*2), transfer ); + } + if ( pBuffer1 ) + { + transfer.pOutput = (short *)pBuffer1; + TransferSamplesToSurroundBuffer( size1 / (channelCount*2), transfer ); + } + pDSBuf->Unlock(pBuffer0, size0, pBuffer1, size1); +} + diff --git a/engine/audio/private/snd_dev_direct.h b/engine/audio/private/snd_dev_direct.h new file mode 100644 index 0000000..428ff4d --- /dev/null +++ b/engine/audio/private/snd_dev_direct.h @@ -0,0 +1,14 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_DIRECT_H +#define SND_DEV_DIRECT_H +#pragma once + +class IAudioDevice; +IAudioDevice *Audio_CreateDirectSoundDevice( void ); + +#endif // SND_DEV_DIRECT_H diff --git a/engine/audio/private/snd_dev_mac_audioqueue.cpp b/engine/audio/private/snd_dev_mac_audioqueue.cpp new file mode 100644 index 0000000..e58a703 --- /dev/null +++ b/engine/audio/private/snd_dev_mac_audioqueue.cpp @@ -0,0 +1,599 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "audio_pch.h" +#include <AudioToolbox/AudioQueue.h> +#include <AudioToolbox/AudioFile.h> +#include <AudioToolbox/AudioFormat.h> + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool snd_firsttime; +extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); +extern void S_SpatializeChannel( int volume[6], int master_vol, const Vector *psourceDir, float gain, float mono ); + +#define NUM_BUFFERS_SOURCES 128 +#define BUFF_MASK (NUM_BUFFERS_SOURCES - 1 ) +#define BUFFER_SIZE 0x0400 + + +//----------------------------------------------------------------------------- +// +// NOTE: This only allows 16-bit, stereo wave out +// +//----------------------------------------------------------------------------- +class CAudioDeviceAudioQueue : public CAudioDeviceBase +{ +public: + bool IsActive( void ); + bool Init( void ); + void Shutdown( void ); + void PaintEnd( void ); + int GetOutputPosition( void ); + void ChannelReset( int entnum, int channelIndex, float distanceMod ); + void Pause( void ); + void UnPause( void ); + float MixDryVolume( void ); + bool Should3DMix( void ); + void StopAllSounds( void ); + + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + void ClearBuffer( void ); + void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ); + void MixBegin( int sampleCount ); + void MixUpsample( int sampleCount, int filtertype ); + void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + + void TransferSamples( int end ); + void SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono); + void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ); + + const char *DeviceName( void ) { return "AudioQueue"; } + int DeviceChannels( void ) { return 2; } + int DeviceSampleBits( void ) { return 16; } + int DeviceSampleBytes( void ) { return 2; } + int DeviceDmaSpeed( void ) { return SOUND_DMA_SPEED; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + + void BufferCompleted() { m_buffersCompleted++; } + void SetRunning( bool bState ) { m_bRunning = bState; } + +private: + void OpenWaveOut( void ); + void CloseWaveOut( void ); + bool ValidWaveOut( void ) const; + bool BIsPlaying(); + + AudioStreamBasicDescription m_DataFormat; + AudioQueueRef m_Queue; + AudioQueueBufferRef m_Buffers[NUM_BUFFERS_SOURCES]; + + int m_SndBufSize; + + void *m_sndBuffers; + + CInterlockedInt m_deviceSampleCount; + + int m_buffersSent; + int m_buffersCompleted; + int m_pauseCount; + bool m_bSoundsShutdown; + + bool m_bFailed; + bool m_bRunning; + + +}; + +CAudioDeviceAudioQueue *wave = NULL; + + +static void AudioCallback(void *pContext, AudioQueueRef pQueue, AudioQueueBufferRef pBuffer) +{ + if ( wave ) + wave->BufferCompleted(); +} + + +IAudioDevice *Audio_CreateMacAudioQueueDevice( void ) +{ + wave = new CAudioDeviceAudioQueue; + if ( wave->Init() ) + return wave; + + delete wave; + wave = NULL; + + return NULL; +} + + +void OnSndSurroundCvarChanged2( IConVar *pVar, const char *pOldString, float flOldValue ); +void OnSndSurroundLegacyChanged2( IConVar *pVar, const char *pOldString, float flOldValue ); + +//----------------------------------------------------------------------------- +// Init, shutdown +//----------------------------------------------------------------------------- +bool CAudioDeviceAudioQueue::Init( void ) +{ + m_SndBufSize = 0; + m_sndBuffers = NULL; + m_pauseCount = 0; + + m_bSurround = false; + m_bSurroundCenter = false; + m_bHeadphone = false; + m_buffersSent = 0; + m_buffersCompleted = 0; + m_pauseCount = 0; + m_bSoundsShutdown = false; + m_bFailed = false; + m_bRunning = false; + + m_Queue = NULL; + + static bool first = true; + if ( first ) + { + snd_surround.SetValue( 2 ); + snd_surround.InstallChangeCallback( &OnSndSurroundCvarChanged2 ); + snd_legacy_surround.InstallChangeCallback( &OnSndSurroundLegacyChanged2 ); + first = false; + } + + OpenWaveOut(); + + if ( snd_firsttime ) + { + DevMsg( "Wave sound initialized\n" ); + } + return ValidWaveOut() && !m_bFailed; +} + +void CAudioDeviceAudioQueue::Shutdown( void ) +{ + CloseWaveOut(); +} + + +//----------------------------------------------------------------------------- +// WAV out device +//----------------------------------------------------------------------------- +inline bool CAudioDeviceAudioQueue::ValidWaveOut( void ) const +{ + return m_sndBuffers != 0 && m_Queue; +} + + +//----------------------------------------------------------------------------- +// called by the mac audioqueue code when we run out of playback buffers +//----------------------------------------------------------------------------- +void AudioQueueIsRunningCallback( void* inClientData, AudioQueueRef inAQ, AudioQueuePropertyID inID) +{ + CAudioDeviceAudioQueue* audioqueue = (CAudioDeviceAudioQueue*)inClientData; + + UInt32 running = 0; + UInt32 size; + OSStatus err = AudioQueueGetProperty(inAQ, kAudioQueueProperty_IsRunning, &running, &size); + audioqueue->SetRunning( running != 0 ); + //DevWarning( "AudioQueueStart %d\n", running ); +} + + + + +//----------------------------------------------------------------------------- +// Opens the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceAudioQueue::OpenWaveOut( void ) +{ + if ( m_Queue ) + return; + + m_buffersSent = 0; + m_buffersCompleted = 0; + + m_DataFormat.mSampleRate = 44100; + m_DataFormat.mFormatID = kAudioFormatLinearPCM; + m_DataFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked; + m_DataFormat.mBytesPerPacket = 4; // 16-bit samples * 2 channels + m_DataFormat.mFramesPerPacket = 1; + m_DataFormat.mBytesPerFrame = 4; // 16-bit samples * 2 channels + m_DataFormat.mChannelsPerFrame = 2; + m_DataFormat.mBitsPerChannel = 16; + m_DataFormat.mReserved = 0; + + // Create the audio queue that will be used to manage the array of audio + // buffers used to queue samples. + OSStatus err = AudioQueueNewOutput(&m_DataFormat, AudioCallback, this, NULL, NULL, 0, &m_Queue); + if ( err != noErr) + { + DevMsg( "Failed to create AudioQueue output %d\n", (int)err ); + m_bFailed = true; + return; + } + + for ( int i = 0; i < NUM_BUFFERS_SOURCES; ++i) + { + err = AudioQueueAllocateBuffer( m_Queue, BUFFER_SIZE,&(m_Buffers[i])); + if ( err != noErr) + { + DevMsg( "Failed to AudioQueueAllocateBuffer output %d (%i)\n",(int)err,i ); + m_bFailed = true; + } + + m_Buffers[i]->mAudioDataByteSize = BUFFER_SIZE; + Q_memset( m_Buffers[i]->mAudioData, 0, BUFFER_SIZE ); + } + + err = AudioQueuePrime( m_Queue, 0, NULL); + if ( err != noErr) + { + DevMsg( "Failed to create AudioQueue output %d\n", (int)err ); + m_bFailed = true; + return; + } + + AudioQueueSetParameter( m_Queue, kAudioQueueParam_Volume, 1.0); + + err = AudioQueueAddPropertyListener( m_Queue, kAudioQueueProperty_IsRunning, AudioQueueIsRunningCallback, this ); + if ( err != noErr) + { + DevMsg( "Failed to create AudioQueue output %d\n", (int)err ); + m_bFailed = true; + return; + } + + m_SndBufSize = NUM_BUFFERS_SOURCES*BUFFER_SIZE; + m_deviceSampleCount = m_SndBufSize / DeviceSampleBytes(); + + if ( !m_sndBuffers ) + { + m_sndBuffers = malloc( m_SndBufSize ); + memset( m_sndBuffers, 0x0, m_SndBufSize ); + } +} + + +//----------------------------------------------------------------------------- +// Closes the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceAudioQueue::CloseWaveOut( void ) +{ + if ( ValidWaveOut() ) + { + AudioQueueStop(m_Queue, true); + m_bRunning = false; + + AudioQueueRemovePropertyListener( m_Queue, kAudioQueueProperty_IsRunning, AudioQueueIsRunningCallback, this ); + + for ( int i = 0; i < NUM_BUFFERS_SOURCES; i++ ) + AudioQueueFreeBuffer( m_Queue, m_Buffers[i]); + + AudioQueueDispose( m_Queue, true); + + m_Queue = NULL; + } + + if ( m_sndBuffers ) + { + free( m_sndBuffers ); + m_sndBuffers = NULL; + } +} + + + +//----------------------------------------------------------------------------- +// Mixing setup +//----------------------------------------------------------------------------- +int CAudioDeviceAudioQueue::PaintBegin( float mixAheadTime, int soundtime, int paintedtime ) +{ + // soundtime - total samples that have been played out to hardware at dmaspeed + // paintedtime - total samples that have been mixed at speed + // endtime - target for samples in mixahead buffer at speed + + unsigned int endtime = soundtime + mixAheadTime * DeviceDmaSpeed(); + + int samps = DeviceSampleCount() >> (DeviceChannels()-1); + + if ((int)(endtime - soundtime) > samps) + endtime = soundtime + samps; + + if ((endtime - paintedtime) & 0x3) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + endtime -= (endtime - paintedtime) & 0x3; + } + + return endtime; +} + + +//----------------------------------------------------------------------------- +// Actually performs the mixing +//----------------------------------------------------------------------------- +void CAudioDeviceAudioQueue::PaintEnd( void ) +{ + int cblocks = 4 << 1; + + if ( m_bRunning && m_buffersSent == m_buffersCompleted ) + { + // We are running the audio queue but have become starved of buffers. + // Stop the audio queue so we force a restart of it. + AudioQueueStop( m_Queue, true ); + } + + // + // submit a few new sound blocks + // + // 44K sound support + while (((m_buffersSent - m_buffersCompleted) >> SAMPLE_16BIT_SHIFT) < cblocks) + { + int iBuf = m_buffersSent&BUFF_MASK; + + m_Buffers[iBuf]->mAudioDataByteSize = BUFFER_SIZE; + Q_memcpy( m_Buffers[iBuf]->mAudioData, (char *)m_sndBuffers + iBuf*BUFFER_SIZE, BUFFER_SIZE); + + // Queue the buffer for playback. + OSStatus err = AudioQueueEnqueueBuffer( m_Queue, m_Buffers[iBuf], 0, NULL); + if ( err != noErr) + { + DevMsg( "Failed to AudioQueueEnqueueBuffer output %d\n", (int)err ); + } + + m_buffersSent++; + } + + + if ( !m_bRunning ) + { + DevMsg( "Restarting sound playback\n" ); + m_bRunning = true; + AudioQueueStart( m_Queue, NULL); + } + +} + +int CAudioDeviceAudioQueue::GetOutputPosition( void ) +{ + int s = m_buffersSent * BUFFER_SIZE; + + s >>= SAMPLE_16BIT_SHIFT; + + s &= (DeviceSampleCount()-1); + + return s / DeviceChannels(); +} + + +//----------------------------------------------------------------------------- +// Pausing +//----------------------------------------------------------------------------- +void CAudioDeviceAudioQueue::Pause( void ) +{ + m_pauseCount++; + if (m_pauseCount == 1) + { + m_bRunning = false; + AudioQueueStop(m_Queue, true); + } +} + + +void CAudioDeviceAudioQueue::UnPause( void ) +{ + if ( m_pauseCount > 0 ) + { + m_pauseCount--; + } + + if ( m_pauseCount == 0 ) + { + m_bRunning = true; + AudioQueueStart( m_Queue, NULL); + } +} + +bool CAudioDeviceAudioQueue::IsActive( void ) +{ + return ( m_pauseCount == 0 ); +} + +float CAudioDeviceAudioQueue::MixDryVolume( void ) +{ + return 0; +} + + +bool CAudioDeviceAudioQueue::Should3DMix( void ) +{ + return false; +} + + +void CAudioDeviceAudioQueue::ClearBuffer( void ) +{ + if ( !m_sndBuffers ) + return; + + Q_memset( m_sndBuffers, 0x0, DeviceSampleCount() * DeviceSampleBytes() ); +} + +void CAudioDeviceAudioQueue::UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) +{ +} + + +bool CAudioDeviceAudioQueue::BIsPlaying() +{ + UInt32 isRunning; + UInt32 propSize = sizeof(isRunning); + + OSStatus result = AudioQueueGetProperty( m_Queue, kAudioQueueProperty_IsRunning, &isRunning, &propSize); + return isRunning != 0; +} + + +void CAudioDeviceAudioQueue::MixBegin( int sampleCount ) +{ + MIX_ClearAllPaintBuffers( sampleCount, false ); +} + + +void CAudioDeviceAudioQueue::MixUpsample( int sampleCount, int filtertype ) +{ + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + int ifilter = ppaint->ifilter; + + Assert (ifilter < CPAINTFILTERS); + + S_MixBufferUpsample2x( sampleCount, ppaint->pbuf, &(ppaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + ppaint->ifilter++; +} + +void CAudioDeviceAudioQueue::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1)) + return; + + Mix8MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceAudioQueue::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix8StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceAudioQueue::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1 )) + return; + + Mix16MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceAudioQueue::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix16StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceAudioQueue::ChannelReset( int entnum, int channelIndex, float distanceMod ) +{ +} + + +void CAudioDeviceAudioQueue::TransferSamples( int end ) +{ + int lpaintedtime = g_paintedtime; + int endtime = end; + + // resumes playback... + + if ( m_sndBuffers ) + { + S_TransferStereo16( m_sndBuffers, PAINTBUFFER, lpaintedtime, endtime ); + } +} + +void CAudioDeviceAudioQueue::SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) +{ + VPROF("CAudioDeviceAudioQueue::SpatializeChannel"); + S_SpatializeChannel( volume, master_vol, &sourceDir, gain, mono ); +} + +void CAudioDeviceAudioQueue::StopAllSounds( void ) +{ + m_bSoundsShutdown = true; + m_bRunning = false; + AudioQueueStop(m_Queue, true); +} + + + +void CAudioDeviceAudioQueue::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) +{ + //SX_RoomFX( endtime, filter, timefx ); + DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount ); +} + + +static uint32 GetOSXSpeakerConfig() +{ + return 2; +} + +static uint32 GetSpeakerConfigForSurroundMode( int surroundMode, const char **pConfigDesc ) +{ + uint32 newSpeakerConfig = 2; + *pConfigDesc = "stereo speaker"; + return newSpeakerConfig; +} + + + +void OnSndSurroundCvarChanged2( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + // if the old value is -1, we're setting this from the detect routine for the first time + // no need to reset the device + if ( flOldValue == -1 ) + return; + + // get the user's previous speaker config + uint32 speaker_config = GetOSXSpeakerConfig(); + + // get the new config + uint32 newSpeakerConfig = 0; + const char *speakerConfigDesc = ""; + + ConVarRef var( pVar ); + newSpeakerConfig = GetSpeakerConfigForSurroundMode( var.GetInt(), &speakerConfigDesc ); + // make sure the config has changed + if (newSpeakerConfig == speaker_config) + return; + + // set new configuration + //SetWindowsSpeakerConfig(newSpeakerConfig); + + Msg("Speaker configuration has been changed to %s.\n", speakerConfigDesc); + + // restart sound system so it takes effect + //g_pSoundServices->RestartSoundSystem(); +} + +void OnSndSurroundLegacyChanged2( IConVar *pVar, const char *pOldString, float flOldValue ) +{ +} + + diff --git a/engine/audio/private/snd_dev_mac_audioqueue.h b/engine/audio/private/snd_dev_mac_audioqueue.h new file mode 100644 index 0000000..7de1260 --- /dev/null +++ b/engine/audio/private/snd_dev_mac_audioqueue.h @@ -0,0 +1,14 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_MAC_AUDIOQUEUE_H +#define SND_DEV_MAC_AUDIOQUEUE_H +#pragma once + +class IAudioDevice; +IAudioDevice *Audio_CreateMacAudioQueueDevice( void ); + +#endif // SND_DEV_MAC_AUDIOQUEUE_H diff --git a/engine/audio/private/snd_dev_openal.cpp b/engine/audio/private/snd_dev_openal.cpp new file mode 100644 index 0000000..82c6136 --- /dev/null +++ b/engine/audio/private/snd_dev_openal.cpp @@ -0,0 +1,611 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "audio_pch.h" +#include <OpenAL/al.h> +#include <OpenAL/alc.h> +#ifdef OSX +#include <OpenAL/MacOSX_OALExtensions.h> +#endif + + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifndef DEDICATED // have to test this because VPC is forcing us to compile this file. + +extern bool snd_firsttime; +extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); +extern void S_SpatializeChannel( int volume[6], int master_vol, const Vector *psourceDir, float gain, float mono ); + +#define NUM_BUFFERS_SOURCES 128 +#define BUFF_MASK (NUM_BUFFERS_SOURCES - 1 ) +#define BUFFER_SIZE 0x0400 + + +//----------------------------------------------------------------------------- +// +// NOTE: This only allows 16-bit, stereo wave out +// +//----------------------------------------------------------------------------- +class CAudioDeviceOpenAL : public CAudioDeviceBase +{ +public: + bool IsActive( void ); + bool Init( void ); + void Shutdown( void ); + void PaintEnd( void ); + int GetOutputPosition( void ); + void ChannelReset( int entnum, int channelIndex, float distanceMod ); + void Pause( void ); + void UnPause( void ); + float MixDryVolume( void ); + bool Should3DMix( void ); + void StopAllSounds( void ); + + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + void ClearBuffer( void ); + void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ); + void MixBegin( int sampleCount ); + void MixUpsample( int sampleCount, int filtertype ); + void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + + void TransferSamples( int end ); + void SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono); + void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ); + + const char *DeviceName( void ) { return "OpenAL"; } + int DeviceChannels( void ) { return 2; } + int DeviceSampleBits( void ) { return 16; } + int DeviceSampleBytes( void ) { return 2; } + int DeviceDmaSpeed( void ) { return SOUND_DMA_SPEED; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + +private: + void OpenWaveOut( void ); + void CloseWaveOut( void ); + bool ValidWaveOut( void ) const; + + ALuint m_Buffer[NUM_BUFFERS_SOURCES]; + ALuint m_Source[1]; + int m_SndBufSize; + + void *m_sndBuffers; + + int m_deviceSampleCount; + + int m_buffersSent; + int m_buffersCompleted; + int m_pauseCount; + bool m_bSoundsShutdown; +}; + + +IAudioDevice *Audio_CreateOpenALDevice( void ) +{ + CAudioDeviceOpenAL *wave = NULL; + if ( !wave ) + { + wave = new CAudioDeviceOpenAL; + } + + if ( wave->Init() ) + return wave; + + delete wave; + wave = NULL; + + return NULL; +} + + +void OnSndSurroundCvarChanged( IConVar *pVar, const char *pOldString, float flOldValue ); +void OnSndSurroundLegacyChanged( IConVar *pVar, const char *pOldString, float flOldValue ); + +//----------------------------------------------------------------------------- +// Init, shutdown +//----------------------------------------------------------------------------- +bool CAudioDeviceOpenAL::Init( void ) +{ + m_SndBufSize = 0; + m_sndBuffers = NULL; + m_pauseCount = 0; + + m_bSurround = false; + m_bSurroundCenter = false; + m_bHeadphone = false; + m_buffersSent = 0; + m_buffersCompleted = 0; + m_pauseCount = 0; + m_bSoundsShutdown = false; + + static bool first = true; + if ( first ) + { + snd_surround.SetValue( 2 ); + snd_surround.InstallChangeCallback( &OnSndSurroundCvarChanged ); + snd_legacy_surround.InstallChangeCallback( &OnSndSurroundLegacyChanged ); + first = false; + } + + OpenWaveOut(); + + if ( snd_firsttime ) + { + DevMsg( "Wave sound initialized\n" ); + } + return ValidWaveOut(); +} + +void CAudioDeviceOpenAL::Shutdown( void ) +{ + CloseWaveOut(); +} + + +//----------------------------------------------------------------------------- +// WAV out device +//----------------------------------------------------------------------------- +inline bool CAudioDeviceOpenAL::ValidWaveOut( void ) const +{ + return m_sndBuffers != 0; +} + + +//----------------------------------------------------------------------------- +// Opens the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceOpenAL::OpenWaveOut( void ) +{ + m_buffersSent = 0; + m_buffersCompleted = 0; + + ALenum error; + ALCcontext *newContext = NULL; + ALCdevice *newDevice = NULL; + + // Create a new OpenAL Device + // Pass NULL to specify the system‚use default output device + const ALCchar *initStr = (const ALCchar *)"\'( (sampling-rate 44100 ))"; + + newDevice = alcOpenDevice(initStr); + if (newDevice != NULL) + { + // Create a new OpenAL Context + // The new context will render to the OpenAL Device just created + ALCint attr[] = { ALC_FREQUENCY, DeviceDmaSpeed(), ALC_SYNC, AL_FALSE, 0 }; + + newContext = alcCreateContext(newDevice, attr ); + if (newContext != NULL) + { + // Make the new context the Current OpenAL Context + alcMakeContextCurrent(newContext); + + // Create some OpenAL Buffer Objects + alGenBuffers( NUM_BUFFERS_SOURCES, m_Buffer); + if((error = alGetError()) != AL_NO_ERROR) + { + DevMsg("Error Generating Buffers: "); + return; + } + + // Create some OpenAL Source Objects + alGenSources(1, m_Source); + if(alGetError() != AL_NO_ERROR) + { + DevMsg("Error generating sources! \n"); + return; + } + + alListener3f( AL_POSITION,0.0f,0.0f,0.0f); + int i; + for ( i = 0; i < 1; i++ ) + { + alSource3f( m_Source[i],AL_POSITION,0.0f,0.0f,0.0f ); + alSourcef( m_Source[i], AL_PITCH, 1.0f ); + alSourcef( m_Source[i], AL_GAIN, 1.0f ); + } + + } + } + + m_SndBufSize = NUM_BUFFERS_SOURCES*BUFFER_SIZE; + m_deviceSampleCount = m_SndBufSize / DeviceSampleBytes(); + + if ( !m_sndBuffers ) + { + m_sndBuffers = malloc( m_SndBufSize ); + memset( m_sndBuffers, 0x0, m_SndBufSize ); + } +} + + +//----------------------------------------------------------------------------- +// Closes the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceOpenAL::CloseWaveOut( void ) +{ + if ( ValidWaveOut() ) + { + ALCcontext *context = NULL; + ALCdevice *device = NULL; + + m_bSoundsShutdown = true; + alSourceStop( m_Source[0] ); + + // Delete the Sources + alDeleteSources(1, m_Source); + // Delete the Buffers + alDeleteBuffers(NUM_BUFFERS_SOURCES, m_Buffer); + + //Get active context + context = alcGetCurrentContext(); + //Get device for active context + device = alcGetContextsDevice(context); + alcMakeContextCurrent( NULL ); + alcSuspendContext(context); + //Release context + alcDestroyContext(context); + //Close device + alcCloseDevice(device); + } + + if ( m_sndBuffers ) + { + free( m_sndBuffers ); + m_sndBuffers = NULL; + } +} + + + +//----------------------------------------------------------------------------- +// Mixing setup +//----------------------------------------------------------------------------- +int CAudioDeviceOpenAL::PaintBegin( float mixAheadTime, int soundtime, int paintedtime ) +{ + // soundtime - total samples that have been played out to hardware at dmaspeed + // paintedtime - total samples that have been mixed at speed + // endtime - target for samples in mixahead buffer at speed + + unsigned int endtime = soundtime + mixAheadTime * DeviceDmaSpeed(); + + int samps = DeviceSampleCount() >> (DeviceChannels()-1); + + if ((int)(endtime - soundtime) > samps) + endtime = soundtime + samps; + + if ((endtime - paintedtime) & 0x3) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + endtime -= (endtime - paintedtime) & 0x3; + } + + return endtime; +} + + +#ifdef OSX +ALvoid alBufferDataStaticProc(const ALint bid, ALenum format, ALvoid* data, ALsizei size, ALsizei freq) +{ + static alBufferDataStaticProcPtr proc = NULL; + + if (proc == NULL) { + proc = (alBufferDataStaticProcPtr) alGetProcAddress((const ALCchar*) "alBufferDataStatic"); + } + + if (proc) + proc(bid, format, data, size, freq); + +} +#endif + + +//----------------------------------------------------------------------------- +// Actually performs the mixing +//----------------------------------------------------------------------------- +void CAudioDeviceOpenAL::PaintEnd( void ) +{ + if ( !m_sndBuffers /*|| m_bSoundsShutdown*/ ) + return; + + ALint state; + ALenum error; + int iloop; + + int cblocks = 4 << 1; + ALint processed = 1; + ALuint lastUnqueuedBuffer = 0; + ALuint unqueuedBuffer = -1; + int nProcessedLoop = 200; // spin for a max of 200 times de-queing buffers, fixes a hang on exit + while ( processed > 0 && --nProcessedLoop > 0 ) + { + alGetSourcei( m_Source[ 0 ], AL_BUFFERS_PROCESSED, &processed); + error = alGetError(); + if (error != AL_NO_ERROR) + break; + + if ( processed > 0 ) + { + lastUnqueuedBuffer = unqueuedBuffer; + alSourceUnqueueBuffers( m_Source[ 0 ], 1, &unqueuedBuffer ); + error = alGetError(); + if ( error != AL_NO_ERROR && error != AL_INVALID_NAME ) + { + DevMsg( "Error alSourceUnqueueBuffers %d\n", error ); + break; + } + else + { + m_buffersCompleted++; // this buffer has been played + } + } + } + + // + // submit a few new sound blocks + // + // 44K sound support + while (((m_buffersSent - m_buffersCompleted) >> SAMPLE_16BIT_SHIFT) < cblocks) + { + int iBuf = m_buffersSent&BUFF_MASK; +#ifdef OSX + alBufferDataStaticProc( m_Buffer[iBuf], AL_FORMAT_STEREO16, (char *)m_sndBuffers + iBuf*BUFFER_SIZE, BUFFER_SIZE, DeviceDmaSpeed() ); +#else + alBufferData( m_Buffer[iBuf], AL_FORMAT_STEREO16, (char *)m_sndBuffers + iBuf*BUFFER_SIZE, BUFFER_SIZE, DeviceDmaSpeed() ); +#endif + if ( (error = alGetError()) != AL_NO_ERROR ) + { + DevMsg( "Error alBufferData %d %d\n", iBuf, error ); + } + + alSourceQueueBuffers( m_Source[0], 1, &m_Buffer[iBuf] ); + if ( (error = alGetError() ) != AL_NO_ERROR ) + { + DevMsg( "Error alSourceQueueBuffers %d %d\n", iBuf, error ); + } + m_buffersSent++; + } + + // make sure the stream is playing + alGetSourcei( m_Source[ 0 ], AL_SOURCE_STATE, &state); + if ( state != AL_PLAYING ) + { + DevMsg( "Restarting sound playback\n" ); + alSourcePlay( m_Source[0] ); + if((error = alGetError()) != AL_NO_ERROR) + { + DevMsg( "Error alSourcePlay %d\n", error ); + } + } +} + +int CAudioDeviceOpenAL::GetOutputPosition( void ) +{ + int s = m_buffersSent * BUFFER_SIZE; + + s >>= SAMPLE_16BIT_SHIFT; + + s &= (DeviceSampleCount()-1); + + return s / DeviceChannels(); +} + + +//----------------------------------------------------------------------------- +// Pausing +//----------------------------------------------------------------------------- +void CAudioDeviceOpenAL::Pause( void ) +{ + m_pauseCount++; + if (m_pauseCount == 1) + { + alSourceStop( m_Source[0] ); + } +} + + +void CAudioDeviceOpenAL::UnPause( void ) +{ + if ( m_pauseCount > 0 ) + { + m_pauseCount--; + } + + if ( m_pauseCount == 0 ) + { + alSourcePlay( m_Source[0] ); + } +} + +bool CAudioDeviceOpenAL::IsActive( void ) +{ + return ( m_pauseCount == 0 ); +} + +float CAudioDeviceOpenAL::MixDryVolume( void ) +{ + return 0; +} + + +bool CAudioDeviceOpenAL::Should3DMix( void ) +{ + return false; +} + + +void CAudioDeviceOpenAL::ClearBuffer( void ) +{ + if ( !m_sndBuffers ) + return; + + Q_memset( m_sndBuffers, 0x0, DeviceSampleCount() * DeviceSampleBytes() ); +} + +void CAudioDeviceOpenAL::UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) +{ +} + + +void CAudioDeviceOpenAL::MixBegin( int sampleCount ) +{ + MIX_ClearAllPaintBuffers( sampleCount, false ); +} + + +void CAudioDeviceOpenAL::MixUpsample( int sampleCount, int filtertype ) +{ + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + int ifilter = ppaint->ifilter; + + Assert (ifilter < CPAINTFILTERS); + + S_MixBufferUpsample2x( sampleCount, ppaint->pbuf, &(ppaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + ppaint->ifilter++; +} + +void CAudioDeviceOpenAL::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1)) + return; + + Mix8MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceOpenAL::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix8StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceOpenAL::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1 )) + return; + + Mix16MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceOpenAL::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix16StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceOpenAL::ChannelReset( int entnum, int channelIndex, float distanceMod ) +{ +} + + +void CAudioDeviceOpenAL::TransferSamples( int end ) +{ + int lpaintedtime = g_paintedtime; + int endtime = end; + + // resumes playback... + + if ( m_sndBuffers ) + { + S_TransferStereo16( m_sndBuffers, PAINTBUFFER, lpaintedtime, endtime ); + } +} + +void CAudioDeviceOpenAL::SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) +{ + VPROF("CAudioDeviceOpenAL::SpatializeChannel"); + S_SpatializeChannel( volume, master_vol, &sourceDir, gain, mono ); +} + +void CAudioDeviceOpenAL::StopAllSounds( void ) +{ + m_bSoundsShutdown = true; + alSourceStop( m_Source[0] ); +} + + + +void CAudioDeviceOpenAL::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) +{ + //SX_RoomFX( endtime, filter, timefx ); + DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount ); +} + + +static uint32 GetOSXSpeakerConfig() +{ + return 2; +} + +static uint32 GetSpeakerConfigForSurroundMode( int surroundMode, const char **pConfigDesc ) +{ + uint32 newSpeakerConfig = 2; + *pConfigDesc = "stereo speaker"; + return newSpeakerConfig; +} + + + +void OnSndSurroundCvarChanged( IConVar *pVar, const char *pOldString, float flOldValue ) +{ + // if the old value is -1, we're setting this from the detect routine for the first time + // no need to reset the device + if ( flOldValue == -1 ) + return; + + // get the user's previous speaker config + uint32 speaker_config = GetOSXSpeakerConfig(); + + // get the new config + uint32 newSpeakerConfig = 0; + const char *speakerConfigDesc = ""; + + ConVarRef var( pVar ); + newSpeakerConfig = GetSpeakerConfigForSurroundMode( var.GetInt(), &speakerConfigDesc ); + // make sure the config has changed + if (newSpeakerConfig == speaker_config) + return; + + // set new configuration + //SetWindowsSpeakerConfig(newSpeakerConfig); + + Msg("Speaker configuration has been changed to %s.\n", speakerConfigDesc); + + // restart sound system so it takes effect + //g_pSoundServices->RestartSoundSystem(); +} + +void OnSndSurroundLegacyChanged( IConVar *pVar, const char *pOldString, float flOldValue ) +{ +} + +#endif // !DEDICATED + diff --git a/engine/audio/private/snd_dev_openal.h b/engine/audio/private/snd_dev_openal.h new file mode 100644 index 0000000..95dc2e6 --- /dev/null +++ b/engine/audio/private/snd_dev_openal.h @@ -0,0 +1,14 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_OPENAL_H +#define SND_DEV_OPENAL_H +#pragma once + +class IAudioDevice; +IAudioDevice *Audio_CreateOpenALDevice( void ); + +#endif // SND_DEV_OPENAL_H diff --git a/engine/audio/private/snd_dev_sdl.cpp b/engine/audio/private/snd_dev_sdl.cpp new file mode 100644 index 0000000..642090f --- /dev/null +++ b/engine/audio/private/snd_dev_sdl.cpp @@ -0,0 +1,574 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "audio_pch.h" + +#if !DEDICATED + +#include "tier0/dynfunction.h" +#include "video//ivideoservices.h" +#include "../../sys_dll.h" + +// prevent some conflicts in SDL headers... +#undef M_PI +#include <stdint.h> +#ifndef _STDINT_H_ +#define _STDINT_H_ 1 +#endif + +#include "SDL.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool snd_firsttime; +extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); +extern void S_SpatializeChannel( /*int nSlot,*/ int volume[6], int master_vol, const Vector *psourceDir, float gain, float mono ); + +// 64K is about 1/3 second at 16-bit, stereo, 44100 Hz +// 44k: UNDONE - need to double buffers now that we're playing back at 44100? +#define WAV_BUFFERS 64 +#define WAV_MASK (WAV_BUFFERS - 1) +#define WAV_BUFFER_SIZE 0x0400 + +#if 0 +#define debugsdl printf +#else +static inline void debugsdl(const char *fmt, ...) {} +#endif + + +//----------------------------------------------------------------------------- +// +// NOTE: This only allows 16-bit, stereo wave out (!!! FIXME: but SDL supports 7.1, etc, too!) +// +//----------------------------------------------------------------------------- +class CAudioDeviceSDLAudio : public CAudioDeviceBase +{ +public: + CAudioDeviceSDLAudio(); + virtual ~CAudioDeviceSDLAudio(); + + bool IsActive( void ); + bool Init( void ); + void Shutdown( void ); + void PaintEnd( void ); + int GetOutputPosition( void ); + void ChannelReset( int entnum, int channelIndex, float distanceMod ); + void Pause( void ); + void UnPause( void ); + float MixDryVolume( void ); + bool Should3DMix( void ); + void StopAllSounds( void ); + + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + void ClearBuffer( void ); + void MixBegin( int sampleCount ); + void MixUpsample( int sampleCount, int filtertype ); + void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + + void TransferSamples( int end ); + void SpatializeChannel( int nSlot, int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono); + void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ); + + const char *DeviceName( void ) { return "SDL"; } + int DeviceChannels( void ) { return 2; } + int DeviceSampleBits( void ) { return 16; } + int DeviceSampleBytes( void ) { return 2; } + int DeviceDmaSpeed( void ) { return SOUND_DMA_SPEED; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + +private: + SDL_AudioDeviceID m_devId; + + static void SDLCALL AudioCallbackEntry(void *userdata, Uint8 * stream, int len); + void AudioCallback(Uint8 *stream, int len); + + void OpenWaveOut( void ); + void CloseWaveOut( void ); + void AllocateOutputBuffers(); + void FreeOutputBuffers(); + bool ValidWaveOut( void ) const; + + int m_deviceSampleCount; + + int m_buffersSent; + int m_pauseCount; + int m_readPos; + int m_partialWrite; + + // Memory for the wave data + uint8_t *m_pBuffer; +}; + +static CAudioDeviceSDLAudio *g_wave = NULL; + +//----------------------------------------------------------------------------- +// Constructor (just lookup SDL entry points, real work happens in this->Init()) +//----------------------------------------------------------------------------- +CAudioDeviceSDLAudio::CAudioDeviceSDLAudio() +{ + m_devId = 0; +} + +//----------------------------------------------------------------------------- +// Destructor. Make sure our global pointer gets set to NULL. +//----------------------------------------------------------------------------- +CAudioDeviceSDLAudio::~CAudioDeviceSDLAudio() +{ + g_wave = NULL; +} + +//----------------------------------------------------------------------------- +// Class factory +//----------------------------------------------------------------------------- +IAudioDevice *Audio_CreateSDLAudioDevice( void ) +{ + if ( !g_wave ) + { + g_wave = new CAudioDeviceSDLAudio; + Assert( g_wave ); + } + + if ( g_wave && !g_wave->Init() ) + { + delete g_wave; + g_wave = NULL; + } + + return g_wave; +} + + +//----------------------------------------------------------------------------- +// Init, shutdown +//----------------------------------------------------------------------------- +bool CAudioDeviceSDLAudio::Init( void ) +{ + // If we've already got a device open, then return. This allows folks to call + // Audio_CreateSDLAudioDevice() multiple times. CloseWaveOut() will free the + // device, and set m_devId to 0. + if( m_devId ) + return true; + + m_bSurround = false; + m_bSurroundCenter = false; + m_bHeadphone = false; + m_buffersSent = 0; + m_pauseCount = 0; + m_pBuffer = NULL; + m_readPos = 0; + m_partialWrite = 0; + m_devId = 0; + + OpenWaveOut(); + + if ( snd_firsttime ) + { + DevMsg( "Wave sound initialized\n" ); + } + + return ValidWaveOut(); +} + +void CAudioDeviceSDLAudio::Shutdown( void ) +{ + CloseWaveOut(); +} + + +//----------------------------------------------------------------------------- +// WAV out device +//----------------------------------------------------------------------------- +inline bool CAudioDeviceSDLAudio::ValidWaveOut( void ) const +{ + return m_devId != 0; +} + + +//----------------------------------------------------------------------------- +// Opens the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::OpenWaveOut( void ) +{ + debugsdl("SDLAUDIO: OpenWaveOut...\n"); + +#ifndef WIN32 + char appname[ 256 ]; + KeyValues *modinfo = new KeyValues( "ModInfo" ); + + if ( modinfo->LoadFromFile( g_pFileSystem, "gameinfo.txt" ) ) + Q_strncpy( appname, modinfo->GetString( "game" ), sizeof( appname ) ); + else + Q_strncpy( appname, "Source1 Game", sizeof( appname ) ); + + modinfo->deleteThis(); + modinfo = NULL; + + // Set these environment variables, in case we're using PulseAudio. + setenv("PULSE_PROP_application.name", appname, 1); + setenv("PULSE_PROP_media.role", "game", 1); +#endif + + // !!! FIXME: specify channel map, etc + // !!! FIXME: set properties (role, icon, etc). + + //#define SDLAUDIO_FAIL(fnstr) do { DevWarning(fnstr " failed"); CloseWaveOut(); return; } while (false) + //#define SDLAUDIO_FAIL(fnstr) do { printf("SDLAUDIO: " fnstr " failed: %s\n", SDL_GetError ? SDL_GetError() : "???"); CloseWaveOut(); return; } while (false) + #define SDLAUDIO_FAIL(fnstr) do { const char *err = SDL_GetError(); printf("SDLAUDIO: " fnstr " failed: %s\n", err ? err : "???"); CloseWaveOut(); return; } while (false) + + if (!SDL_WasInit(SDL_INIT_AUDIO)) + { + if (SDL_InitSubSystem(SDL_INIT_AUDIO)) + SDLAUDIO_FAIL("SDL_InitSubSystem(SDL_INIT_AUDIO)"); + } + + debugsdl("SDLAUDIO: Using SDL audio target '%s'\n", SDL_GetCurrentAudioDriver()); + + // Open an audio device... + // !!! FIXME: let user specify a device? + // !!! FIXME: we can handle quad, 5.1, 7.1, etc here. + SDL_AudioSpec desired, obtained; + memset(&desired, '\0', sizeof (desired)); + desired.freq = SOUND_DMA_SPEED; + desired.format = AUDIO_S16SYS; + desired.channels = 2; + desired.samples = 2048; + desired.callback = &CAudioDeviceSDLAudio::AudioCallbackEntry; + desired.userdata = this; + m_devId = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, SDL_AUDIO_ALLOW_ANY_CHANGE); + + if (!m_devId) + SDLAUDIO_FAIL("SDL_OpenAudioDevice()"); + + #undef SDLAUDIO_FAIL + + // We're now ready to feed audio data to SDL! + AllocateOutputBuffers(); + SDL_PauseAudioDevice(m_devId, 0); + +#if defined( BINK_VIDEO ) && defined( LINUX ) + // Tells Bink to use SDL for its audio decoding + if ( g_pVideo != NULL) + { + g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::SET_SDL_PARAMS, NULL, (void *)&obtained ); + + } +#endif +} + +//----------------------------------------------------------------------------- +// Closes the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::CloseWaveOut( void ) +{ + // none of these SDL_* functions are available to call if this is false. + if (m_devId) + { + SDL_CloseAudioDevice(m_devId); + m_devId = 0; + } + SDL_QuitSubSystem(SDL_INIT_AUDIO); + FreeOutputBuffers(); +} + +//----------------------------------------------------------------------------- +// Allocate output buffers +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::AllocateOutputBuffers() +{ + // Allocate and lock memory for the waveform data. + const int nBufferSize = WAV_BUFFER_SIZE * WAV_BUFFERS; + m_pBuffer = new uint8_t[nBufferSize]; + memset(m_pBuffer, '\0', nBufferSize); + m_readPos = 0; + m_partialWrite = 0; + m_deviceSampleCount = nBufferSize / DeviceSampleBytes(); +} + + +//----------------------------------------------------------------------------- +// Free output buffers +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::FreeOutputBuffers() +{ + delete[] m_pBuffer; + m_pBuffer = NULL; +} + + +//----------------------------------------------------------------------------- +// Mixing setup +//----------------------------------------------------------------------------- +int CAudioDeviceSDLAudio::PaintBegin( float mixAheadTime, int soundtime, int paintedtime ) +{ + // soundtime - total samples that have been played out to hardware at dmaspeed + // paintedtime - total samples that have been mixed at speed + // endtime - target for samples in mixahead buffer at speed + unsigned int endtime = soundtime + mixAheadTime * DeviceDmaSpeed(); + + int samps = DeviceSampleCount() >> (DeviceChannels()-1); + + if ((int)(endtime - soundtime) > samps) + endtime = soundtime + samps; + + if ((endtime - paintedtime) & 0x3) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + endtime -= (endtime - paintedtime) & 0x3; + } + + return endtime; +} + +void CAudioDeviceSDLAudio::AudioCallbackEntry(void *userdata, Uint8 *stream, int len) +{ + ((CAudioDeviceSDLAudio *) userdata)->AudioCallback(stream, len); +} + +void CAudioDeviceSDLAudio::AudioCallback(Uint8 *stream, int len) +{ + if (!m_devId) + { + debugsdl("SDLAUDIO: uhoh, no audio device!\n"); + return; // can this even happen? + } + + const int totalWriteable = len; +#if defined( BINK_VIDEO ) && defined( LINUX ) + Uint8 *stream_orig = stream; +#endif + debugsdl("SDLAUDIO: writable size is %d.\n", totalWriteable); + + Assert(len <= (WAV_BUFFERS * WAV_BUFFER_SIZE)); + + while (len > 0) + { + // spaceAvailable == bytes before we overrun the end of the ring buffer. + const int spaceAvailable = ((WAV_BUFFERS * WAV_BUFFER_SIZE) - m_readPos); + const int writeLen = (len < spaceAvailable) ? len : spaceAvailable; + + if (writeLen > 0) + { + const uint8_t *buf = m_pBuffer + m_readPos; + debugsdl("SDLAUDIO: Writing %d bytes...\n", writeLen); + + #if 0 + static FILE *io = NULL; + if (io == NULL) io = fopen("dumpplayback.raw", "wb"); + if (io != NULL) { fwrite(buf, writeLen, 1, io); fflush(io); } + #endif + + memcpy(stream, buf, writeLen); + stream += writeLen; + len -= writeLen; + Assert(len >= 0); + } + + m_readPos = len ? 0 : (m_readPos + writeLen); // if still bytes to write to stream, we're rolling around the ring buffer. + } + +#if defined( BINK_VIDEO ) && defined( LINUX ) + // Mix in Bink movie audio if that stuff is playing. + if ( g_pVideo != NULL) + { + g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::SDLMIXER_CALLBACK, (void *)stream_orig, (void *)&totalWriteable ); + } +#endif + + // Translate between bytes written and buffers written. + m_partialWrite += totalWriteable; + m_buffersSent += m_partialWrite / WAV_BUFFER_SIZE; + m_partialWrite %= WAV_BUFFER_SIZE; +} + + +//----------------------------------------------------------------------------- +// Actually performs the mixing +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::PaintEnd( void ) +{ + debugsdl("SDLAUDIO: PaintEnd...\n"); + +#if 0 // !!! FIXME: this is the 1.3 headers, but not implemented yet in SDL. + if (SDL_AudioDeviceConnected(m_devId) != 1) + { + debugsdl("SDLAUDIO: Audio device was disconnected!\n"); + Shutdown(); + } +#endif +} + +int CAudioDeviceSDLAudio::GetOutputPosition( void ) +{ + return (m_readPos >> SAMPLE_16BIT_SHIFT)/DeviceChannels(); +} + + +//----------------------------------------------------------------------------- +// Pausing +//----------------------------------------------------------------------------- +void CAudioDeviceSDLAudio::Pause( void ) +{ + m_pauseCount++; + if (m_pauseCount == 1) + { + debugsdl("SDLAUDIO: PAUSE\n"); + SDL_PauseAudioDevice(m_devId, 1); + } +} + + +void CAudioDeviceSDLAudio::UnPause( void ) +{ + if ( m_pauseCount > 0 ) + { + m_pauseCount--; + if (m_pauseCount == 0) + { + debugsdl("SDLAUDIO: UNPAUSE\n"); + SDL_PauseAudioDevice(m_devId, 0); + } + } +} + +bool CAudioDeviceSDLAudio::IsActive( void ) +{ + return ( m_pauseCount == 0 ); +} + +float CAudioDeviceSDLAudio::MixDryVolume( void ) +{ + return 0; +} + + +bool CAudioDeviceSDLAudio::Should3DMix( void ) +{ + return false; +} + + +void CAudioDeviceSDLAudio::ClearBuffer( void ) +{ + int clear; + + if ( !m_pBuffer ) + return; + + clear = 0; + + Q_memset(m_pBuffer, clear, DeviceSampleCount() * DeviceSampleBytes() ); +} + + +void CAudioDeviceSDLAudio::MixBegin( int sampleCount ) +{ + MIX_ClearAllPaintBuffers( sampleCount, false ); +} + + +void CAudioDeviceSDLAudio::MixUpsample( int sampleCount, int filtertype ) +{ + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + int ifilter = ppaint->ifilter; + + Assert (ifilter < CPAINTFILTERS); + + S_MixBufferUpsample2x( sampleCount, ppaint->pbuf, &(ppaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + ppaint->ifilter++; +} + +void CAudioDeviceSDLAudio::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1)) + return; + + Mix8MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceSDLAudio::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix8StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceSDLAudio::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1 )) + return; + + Mix16MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceSDLAudio::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix16StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceSDLAudio::ChannelReset( int entnum, int channelIndex, float distanceMod ) +{ +} + + +void CAudioDeviceSDLAudio::TransferSamples( int end ) +{ + int lpaintedtime = g_paintedtime; + int endtime = end; + + // resumes playback... + + if ( m_pBuffer ) + { + S_TransferStereo16( m_pBuffer, PAINTBUFFER, lpaintedtime, endtime ); + } +} + +void CAudioDeviceSDLAudio::SpatializeChannel( int nSlot, int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) +{ + VPROF("CAudioDeviceSDLAudio::SpatializeChannel"); + S_SpatializeChannel( /*nSlot,*/ volume, master_vol, &sourceDir, gain, mono ); +} + +void CAudioDeviceSDLAudio::StopAllSounds( void ) +{ +} + + +void CAudioDeviceSDLAudio::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) +{ + //SX_RoomFX( endtime, filter, timefx ); + DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount ); +} + +#endif // !DEDICATED + diff --git a/engine/audio/private/snd_dev_sdl.h b/engine/audio/private/snd_dev_sdl.h new file mode 100644 index 0000000..ce0af10 --- /dev/null +++ b/engine/audio/private/snd_dev_sdl.h @@ -0,0 +1,16 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_SDL_H +#define SND_DEV_SDL_H +#pragma once + +class IAudioDevice; +IAudioDevice *Audio_CreateSDLAudioDevice( void ); + +#endif // SND_DEV_SDL_H + + diff --git a/engine/audio/private/snd_dev_wave.cpp b/engine/audio/private/snd_dev_wave.cpp new file mode 100644 index 0000000..92cb439 --- /dev/null +++ b/engine/audio/private/snd_dev_wave.cpp @@ -0,0 +1,570 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "audio_pch.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool snd_firsttime; +extern bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ); +extern void S_SpatializeChannel( int volume[6], int master_vol, const Vector *psourceDir, float gain, float mono ); + +// 64K is > 1 second at 16-bit, 22050 Hz +// 44k: UNDONE - need to double buffers now that we're playing back at 44100? +#define WAV_BUFFERS 64 +#define WAV_MASK 0x3F +#define WAV_BUFFER_SIZE 0x0400 + + +//----------------------------------------------------------------------------- +// +// NOTE: This only allows 16-bit, stereo wave out +// +//----------------------------------------------------------------------------- +class CAudioDeviceWave : public CAudioDeviceBase +{ +public: + bool IsActive( void ); + bool Init( void ); + void Shutdown( void ); + void PaintEnd( void ); + int GetOutputPosition( void ); + void ChannelReset( int entnum, int channelIndex, float distanceMod ); + void Pause( void ); + void UnPause( void ); + float MixDryVolume( void ); + bool Should3DMix( void ); + void StopAllSounds( void ); + + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + void ClearBuffer( void ); + void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ); + void MixBegin( int sampleCount ); + void MixUpsample( int sampleCount, int filtertype ); + void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ); + + void TransferSamples( int end ); + void SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono); + void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ); + + const char *DeviceName( void ) { return "Windows WAVE"; } + int DeviceChannels( void ) { return 2; } + int DeviceSampleBits( void ) { return 16; } + int DeviceSampleBytes( void ) { return 2; } + int DeviceDmaSpeed( void ) { return SOUND_DMA_SPEED; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + +private: + void OpenWaveOut( void ); + void CloseWaveOut( void ); + void AllocateOutputBuffers(); + void FreeOutputBuffers(); + void* AllocOutputMemory( int nSize, HGLOBAL &hMemory ); + void FreeOutputMemory( HGLOBAL &hMemory ); + bool ValidWaveOut( void ) const; + + int m_deviceSampleCount; + + int m_buffersSent; + int m_buffersCompleted; + int m_pauseCount; + + // This is a single allocation for all wave headers (there are OUTPUT_BUFFER_COUNT of them) + HGLOBAL m_hWaveHdr; + + // This is a single allocation for all wave data (there are OUTPUT_BUFFER_COUNT of them) + HANDLE m_hWaveData; + + HWAVEOUT m_waveOutHandle; + + // Memory for the wave data + wave headers + void *m_pBuffer; + LPWAVEHDR m_pWaveHdr; +}; + + +//----------------------------------------------------------------------------- +// Class factory +//----------------------------------------------------------------------------- +IAudioDevice *Audio_CreateWaveDevice( void ) +{ + CAudioDeviceWave *wave = NULL; + if ( !wave ) + { + wave = new CAudioDeviceWave; + } + + if ( wave->Init() ) + return wave; + + delete wave; + wave = NULL; + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Init, shutdown +//----------------------------------------------------------------------------- +bool CAudioDeviceWave::Init( void ) +{ + m_bSurround = false; + m_bSurroundCenter = false; + m_bHeadphone = false; + m_buffersSent = 0; + m_buffersCompleted = 0; + m_pauseCount = 0; + m_waveOutHandle = 0; + m_pBuffer = NULL; + m_pWaveHdr = NULL; + m_hWaveHdr = NULL; + m_hWaveData = NULL; + + OpenWaveOut(); + + if ( snd_firsttime ) + { + DevMsg( "Wave sound initialized\n" ); + } + return ValidWaveOut(); +} + +void CAudioDeviceWave::Shutdown( void ) +{ + CloseWaveOut(); +} + + +//----------------------------------------------------------------------------- +// WAV out device +//----------------------------------------------------------------------------- +inline bool CAudioDeviceWave::ValidWaveOut( void ) const +{ + return m_waveOutHandle != 0; +} + + +//----------------------------------------------------------------------------- +// Opens the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceWave::OpenWaveOut( void ) +{ + WAVEFORMATEX waveFormat; + memset( &waveFormat, 0, sizeof(waveFormat) ); + + // Select a PCM, 16-bit stereo playback device + waveFormat.cbSize = sizeof(waveFormat); + waveFormat.wFormatTag = WAVE_FORMAT_PCM; + waveFormat.nChannels = DeviceChannels(); + waveFormat.wBitsPerSample = DeviceSampleBits(); + waveFormat.nSamplesPerSec = DeviceDmaSpeed(); // DeviceSampleRate + waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample / 8; + waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec * waveFormat.nBlockAlign; + + MMRESULT errorCode = waveOutOpen( &m_waveOutHandle, WAVE_MAPPER, &waveFormat, 0, 0L, CALLBACK_NULL ); + while ( errorCode != MMSYSERR_NOERROR ) + { + if ( errorCode != MMSYSERR_ALLOCATED ) + { + DevWarning( "waveOutOpen failed\n" ); + m_waveOutHandle = 0; + return; + } + + int nRetVal = MessageBox( NULL, + "The sound hardware is in use by another app.\n\n" + "Select Retry to try to start sound again or Cancel to run with no sound.", + "Sound not available", + MB_RETRYCANCEL | MB_SETFOREGROUND | MB_ICONEXCLAMATION); + + if ( nRetVal != IDRETRY ) + { + DevWarning( "waveOutOpen failure--hardware already in use\n" ); + m_waveOutHandle = 0; + return; + } + + errorCode = waveOutOpen( &m_waveOutHandle, WAVE_MAPPER, &waveFormat, 0, 0L, CALLBACK_NULL ); + } + + AllocateOutputBuffers(); +} + + +//----------------------------------------------------------------------------- +// Closes the windows wave out device +//----------------------------------------------------------------------------- +void CAudioDeviceWave::CloseWaveOut( void ) +{ + if ( ValidWaveOut() ) + { + waveOutReset( m_waveOutHandle ); + FreeOutputBuffers(); + waveOutClose( m_waveOutHandle ); + m_waveOutHandle = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Alloc output memory +//----------------------------------------------------------------------------- +void* CAudioDeviceWave::AllocOutputMemory( int nSize, HGLOBAL &hMemory ) +{ + // Output memory for waveform data+hdrs must be + // globally allocated with GMEM_MOVEABLE and GMEM_SHARE flags. + hMemory = GlobalAlloc( GMEM_MOVEABLE | GMEM_SHARE, nSize ); + if ( !hMemory ) + { + DevWarning( "Sound: Out of memory.\n"); + CloseWaveOut(); + return NULL; + } + + HPSTR lpData = (char *)GlobalLock( hMemory ); + if ( !lpData ) + { + DevWarning( "Sound: Failed to lock.\n"); + GlobalFree( hMemory ); + hMemory = NULL; + CloseWaveOut(); + return NULL; + } + memset( lpData, 0, nSize ); + return lpData; +} + + +//----------------------------------------------------------------------------- +// Free output memory +//----------------------------------------------------------------------------- +void CAudioDeviceWave::FreeOutputMemory( HGLOBAL &hMemory ) +{ + if ( hMemory ) + { + GlobalUnlock( hMemory ); + GlobalFree( hMemory ); + hMemory = NULL; + } +} + + +//----------------------------------------------------------------------------- +// Allocate output buffers +//----------------------------------------------------------------------------- +void CAudioDeviceWave::AllocateOutputBuffers() +{ + // Allocate and lock memory for the waveform data. + int nBufferSize = WAV_BUFFER_SIZE * WAV_BUFFERS; + HPSTR lpData = (char *)AllocOutputMemory( nBufferSize, m_hWaveData ); + if ( !lpData ) + return; + + // Allocate and lock memory for the waveform header + int nHdrSize = sizeof( WAVEHDR ) * WAV_BUFFERS; + LPWAVEHDR lpWaveHdr = (LPWAVEHDR)AllocOutputMemory( nHdrSize, m_hWaveHdr ); + if ( !lpWaveHdr ) + return; + + // After allocation, set up and prepare headers. + for ( int i=0 ; i < WAV_BUFFERS; i++ ) + { + LPWAVEHDR lpHdr = lpWaveHdr + i; + lpHdr->dwBufferLength = WAV_BUFFER_SIZE; + lpHdr->lpData = lpData + (i * WAV_BUFFER_SIZE); + + MMRESULT nResult = waveOutPrepareHeader( m_waveOutHandle, lpHdr, sizeof(WAVEHDR) ); + if ( nResult != MMSYSERR_NOERROR ) + { + DevWarning( "Sound: failed to prepare wave headers\n" ); + CloseWaveOut(); + return; + } + } + + m_deviceSampleCount = nBufferSize / DeviceSampleBytes(); + + m_pBuffer = (void *)lpData; + m_pWaveHdr = lpWaveHdr; +} + + +//----------------------------------------------------------------------------- +// Free output buffers +//----------------------------------------------------------------------------- +void CAudioDeviceWave::FreeOutputBuffers() +{ + // Unprepare headers. + if ( m_pWaveHdr ) + { + for ( int i=0 ; i < WAV_BUFFERS; i++ ) + { + waveOutUnprepareHeader( m_waveOutHandle, m_pWaveHdr+i, sizeof(WAVEHDR) ); + } + } + m_pWaveHdr = NULL; + m_pBuffer = NULL; + + FreeOutputMemory( m_hWaveData ); + FreeOutputMemory( m_hWaveHdr ); +} + + +//----------------------------------------------------------------------------- +// Mixing setup +//----------------------------------------------------------------------------- +int CAudioDeviceWave::PaintBegin( float mixAheadTime, int soundtime, int paintedtime ) +{ + // soundtime - total samples that have been played out to hardware at dmaspeed + // paintedtime - total samples that have been mixed at speed + // endtime - target for samples in mixahead buffer at speed + + unsigned int endtime = soundtime + mixAheadTime * DeviceDmaSpeed(); + + int samps = DeviceSampleCount() >> (DeviceChannels()-1); + + if ((int)(endtime - soundtime) > samps) + endtime = soundtime + samps; + + if ((endtime - paintedtime) & 0x3) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + endtime -= (endtime - paintedtime) & 0x3; + } + + return endtime; +} + + +//----------------------------------------------------------------------------- +// Actually performs the mixing +//----------------------------------------------------------------------------- +void CAudioDeviceWave::PaintEnd( void ) +{ + LPWAVEHDR h; + int wResult; + int cblocks; + + // + // find which sound blocks have completed + // + while (1) + { + if ( m_buffersCompleted == m_buffersSent ) + { + //DevMsg ("Sound overrun\n"); + break; + } + + if ( ! (m_pWaveHdr[ m_buffersCompleted & WAV_MASK].dwFlags & WHDR_DONE) ) + { + break; + } + + m_buffersCompleted++; // this buffer has been played + } + + // + // submit a few new sound blocks + // + // 22K sound support + // 44k: UNDONE - double blocks out now that we're at 44k playback? + cblocks = 4 << 1; + + while (((m_buffersSent - m_buffersCompleted) >> SAMPLE_16BIT_SHIFT) < cblocks) + { + h = m_pWaveHdr + ( m_buffersSent&WAV_MASK ); + + m_buffersSent++; + /* + * Now the data block can be sent to the output device. The + * waveOutWrite function returns immediately and waveform + * data is sent to the output device in the background. + */ + wResult = waveOutWrite( m_waveOutHandle, h, sizeof(WAVEHDR) ); + + if (wResult != MMSYSERR_NOERROR) + { + Warning( "Failed to write block to device\n"); + Shutdown(); + return; + } + } +} + +int CAudioDeviceWave::GetOutputPosition( void ) +{ + int s = m_buffersSent * WAV_BUFFER_SIZE; + + s >>= SAMPLE_16BIT_SHIFT; + + s &= (DeviceSampleCount()-1); + + return s / DeviceChannels(); +} + + +//----------------------------------------------------------------------------- +// Pausing +//----------------------------------------------------------------------------- +void CAudioDeviceWave::Pause( void ) +{ + m_pauseCount++; + if (m_pauseCount == 1) + { + waveOutReset( m_waveOutHandle ); + } +} + + +void CAudioDeviceWave::UnPause( void ) +{ + if ( m_pauseCount > 0 ) + { + m_pauseCount--; + } +} + +bool CAudioDeviceWave::IsActive( void ) +{ + return ( m_pauseCount == 0 ); +} + +float CAudioDeviceWave::MixDryVolume( void ) +{ + return 0; +} + + +bool CAudioDeviceWave::Should3DMix( void ) +{ + return false; +} + + +void CAudioDeviceWave::ClearBuffer( void ) +{ + int clear; + + if ( !m_pBuffer ) + return; + + clear = 0; + + Q_memset(m_pBuffer, clear, DeviceSampleCount() * DeviceSampleBytes() ); +} + +void CAudioDeviceWave::UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) +{ +} + + +void CAudioDeviceWave::MixBegin( int sampleCount ) +{ + MIX_ClearAllPaintBuffers( sampleCount, false ); +} + + +void CAudioDeviceWave::MixUpsample( int sampleCount, int filtertype ) +{ + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + int ifilter = ppaint->ifilter; + + Assert (ifilter < CPAINTFILTERS); + + S_MixBufferUpsample2x( sampleCount, ppaint->pbuf, &(ppaint->fltmem[ifilter][0]), CPAINTFILTERMEM, filtertype ); + + ppaint->ifilter++; +} + +void CAudioDeviceWave::Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1)) + return; + + Mix8MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceWave::Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix8StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, (byte *)pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceWave::Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 1 )) + return; + + Mix16MonoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceWave::Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) +{ + int volume[CCHANVOLUMES]; + paintbuffer_t *ppaint = MIX_GetCurrentPaintbufferPtr(); + + if (!MIX_ScaleChannelVolume( ppaint, pChannel, volume, 2 )) + return; + + Mix16StereoWavtype( pChannel, ppaint->pbuf + outputOffset, volume, pData, inputOffset, rateScaleFix, outCount ); +} + + +void CAudioDeviceWave::ChannelReset( int entnum, int channelIndex, float distanceMod ) +{ +} + + +void CAudioDeviceWave::TransferSamples( int end ) +{ + int lpaintedtime = g_paintedtime; + int endtime = end; + + // resumes playback... + + if ( m_pBuffer ) + { + S_TransferStereo16( m_pBuffer, PAINTBUFFER, lpaintedtime, endtime ); + } +} + +void CAudioDeviceWave::SpatializeChannel( int volume[CCHANVOLUMES/2], int master_vol, const Vector& sourceDir, float gain, float mono ) +{ + VPROF("CAudioDeviceWave::SpatializeChannel"); + S_SpatializeChannel( volume, master_vol, &sourceDir, gain, mono ); +} + +void CAudioDeviceWave::StopAllSounds( void ) +{ +} + + +void CAudioDeviceWave::ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) +{ + //SX_RoomFX( endtime, filter, timefx ); + DSP_Process( idsp, pbuffront, pbufrear, pbufcenter, samplecount ); +} diff --git a/engine/audio/private/snd_dev_wave.h b/engine/audio/private/snd_dev_wave.h new file mode 100644 index 0000000..582fc0b --- /dev/null +++ b/engine/audio/private/snd_dev_wave.h @@ -0,0 +1,14 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_WAVE_H +#define SND_DEV_WAVE_H +#pragma once + +class IAudioDevice; +IAudioDevice *Audio_CreateWaveDevice( void ); + +#endif // SND_DEV_WAVE_H diff --git a/engine/audio/private/snd_dev_xaudio.cpp b/engine/audio/private/snd_dev_xaudio.cpp new file mode 100644 index 0000000..1601701 --- /dev/null +++ b/engine/audio/private/snd_dev_xaudio.cpp @@ -0,0 +1,872 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: X360 XAudio Version +// +//=====================================================================================// + + +#include "audio_pch.h" +#include "snd_dev_xaudio.h" +#include "UtlLinkedList.h" +#include "session.h" +#include "server.h" +#include "client.h" +#include "matchmaking.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// The outer code mixes in PAINTBUFFER_SIZE (# of samples) chunks (see MIX_PaintChannels), we will never need more than +// that many samples in a buffer. This ends up being about 20ms per buffer +#define XAUDIO2_BUFFER_SAMPLES 8192 +// buffer return has a latency, so need a decent pool +#define MAX_XAUDIO2_BUFFERS 32 + + +#define SURROUND_HEADPHONES 0 +#define SURROUND_STEREO 2 +#define SURROUND_DIGITAL5DOT1 5 + +// 5.1 means there are a max of 6 channels +#define MAX_DEVICE_CHANNELS 6 + +ConVar snd_xaudio_spew_packets( "snd_xaudio_spew_packets", "0", 0, "Spew XAudio packet delivery" ); + + + +//----------------------------------------------------------------------------- +// Implementation of XAudio +//----------------------------------------------------------------------------- +class CAudioXAudio : public CAudioDeviceBase +{ +public: + ~CAudioXAudio( void ); + + bool IsActive( void ) { return true; } + bool Init( void ); + void Shutdown( void ); + + void Pause( void ); + void UnPause( void ); + int PaintBegin( float mixAheadTime, int soundtime, int paintedtime ); + int GetOutputPosition( void ); + void ClearBuffer( void ); + void TransferSamples( int end ); + + const char *DeviceName( void ); + int DeviceChannels( void ) { return m_deviceChannels; } + int DeviceSampleBits( void ) { return m_deviceSampleBits; } + int DeviceSampleBytes( void ) { return m_deviceSampleBits/8; } + int DeviceDmaSpeed( void ) { return m_deviceDmaSpeed; } + int DeviceSampleCount( void ) { return m_deviceSampleCount; } + + + + void XAudioPacketCallback( int hCompletedBuffer ); + + static CAudioXAudio *m_pSingleton; + + CXboxVoice *GetVoiceData( void ) { return &m_VoiceData; } + IXAudio2 *GetXAudio2( void ) { return m_pXAudio2; } + +private: + int TransferStereo( const portable_samplepair_t *pFront, int paintedTime, int endTime, char *pOutptuBuffer ); + int TransferSurroundInterleaved( const portable_samplepair_t *pFront, const portable_samplepair_t *pRear, const portable_samplepair_t *pCenter, int paintedTime, int endTime, char *pOutputBuffer ); + + int m_deviceChannels; // channels per hardware output buffer (1 for quad/5.1, 2 for stereo) + int m_deviceSampleBits; // bits per sample (16) + int m_deviceSampleCount; // count of mono samples in output buffer + int m_deviceDmaSpeed; // samples per second per output buffer + int m_clockDivider; + + IXAudio2 *m_pXAudio2; + IXAudio2MasteringVoice *m_pMasteringVoice; + IXAudio2SourceVoice *m_pSourceVoice; + + XAUDIO2_BUFFER m_Buffers[MAX_XAUDIO2_BUFFERS]; + BYTE *m_pOutputBuffer; + int m_bufferSizeBytes; // size of a single hardware output buffer, in bytes + CInterlockedUInt m_BufferTail; + CInterlockedUInt m_BufferHead; + + CXboxVoice m_VoiceData; +}; + +CAudioXAudio *CAudioXAudio::m_pSingleton = NULL; + +//----------------------------------------------------------------------------- +// XAudio packet completion callback +//----------------------------------------------------------------------------- +class XAudio2VoiceCallback : public IXAudio2VoiceCallback +{ +public: + XAudio2VoiceCallback() {} + ~XAudio2VoiceCallback() {} + + void OnStreamEnd() {} + + void OnVoiceProcessingPassEnd() {} + + void OnVoiceProcessingPassStart( UINT32 SamplesRequired ) {} + + void OnBufferEnd( void *pBufferContext ) + { + CAudioXAudio::m_pSingleton->XAudioPacketCallback( (int)pBufferContext ); + } + + void OnBufferStart( void *pBufferContext ) {} + + void OnLoopEnd( void *pBufferContext ) {} + + void OnVoiceError( void *pBufferContext, HRESULT Error ) {} +}; +XAudio2VoiceCallback s_XAudio2VoiceCallback; + + + +//----------------------------------------------------------------------------- +// Create XAudio Device +//----------------------------------------------------------------------------- +IAudioDevice *Audio_CreateXAudioDevice( void ) +{ + MEM_ALLOC_CREDIT(); + + if ( !CAudioXAudio::m_pSingleton ) + { + CAudioXAudio::m_pSingleton = new CAudioXAudio; + } + + if ( !CAudioXAudio::m_pSingleton->Init() ) + { + delete CAudioXAudio::m_pSingleton; + CAudioXAudio::m_pSingleton = NULL; + } + + return CAudioXAudio::m_pSingleton; +} + +CXboxVoice *Audio_GetXVoice( void ) +{ + if ( CAudioXAudio::m_pSingleton ) + { + return CAudioXAudio::m_pSingleton->GetVoiceData(); + } + + return NULL; +} + +IXAudio2 *Audio_GetXAudio2( void ) +{ + if ( CAudioXAudio::m_pSingleton ) + { + return CAudioXAudio::m_pSingleton->GetXAudio2(); + } + + return NULL; +} + +//----------------------------------------------------------------------------- +// Destructor +//----------------------------------------------------------------------------- +CAudioXAudio::~CAudioXAudio( void ) +{ + m_pSingleton = NULL; +} + +//----------------------------------------------------------------------------- +// Initialize XAudio +//----------------------------------------------------------------------------- +bool CAudioXAudio::Init( void ) +{ + XAUDIOSPEAKERCONFIG xAudioConfig = 0; + XAudioGetSpeakerConfig( &xAudioConfig ); + snd_surround.SetValue( ( xAudioConfig & XAUDIOSPEAKERCONFIG_DIGITAL_DOLBYDIGITAL ) ? SURROUND_DIGITAL5DOT1 : SURROUND_STEREO ); + + m_bHeadphone = false; + m_bSurround = false; + m_bSurroundCenter = false; + + switch ( snd_surround.GetInt() ) + { + case SURROUND_HEADPHONES: + m_bHeadphone = true; + m_deviceChannels = 2; + break; + + default: + case SURROUND_STEREO: + m_deviceChannels = 2; + break; + + case SURROUND_DIGITAL5DOT1: + m_bSurround = true; + m_bSurroundCenter = true; + m_deviceChannels = 6; + break; + } + + m_deviceSampleBits = 16; + m_deviceDmaSpeed = SOUND_DMA_SPEED; + + // initialize the XAudio Engine + // Both threads on core 2 + m_pXAudio2 = NULL; + HRESULT hr = XAudio2Create( &m_pXAudio2, 0, XboxThread5 ); + if ( FAILED( hr ) ) + return false; + + // create the mastering voice, this will upsample to the devices target hw output rate + m_pMasteringVoice = NULL; + hr = m_pXAudio2->CreateMasteringVoice( &m_pMasteringVoice ); + if ( FAILED( hr ) ) + return false; + + // 16 bit PCM + WAVEFORMATEX waveFormatEx = { 0 }; + waveFormatEx.wFormatTag = WAVE_FORMAT_PCM; + waveFormatEx.nChannels = m_deviceChannels; + waveFormatEx.nSamplesPerSec = m_deviceDmaSpeed; + waveFormatEx.wBitsPerSample = 16; + waveFormatEx.nBlockAlign = ( waveFormatEx.nChannels * waveFormatEx.wBitsPerSample ) / 8; + waveFormatEx.nAvgBytesPerSec = waveFormatEx.nSamplesPerSec * waveFormatEx.nBlockAlign; + waveFormatEx.cbSize = 0; + + m_pSourceVoice = NULL; + hr = m_pXAudio2->CreateSourceVoice( + &m_pSourceVoice, + &waveFormatEx, + 0, + XAUDIO2_DEFAULT_FREQ_RATIO, + &s_XAudio2VoiceCallback, + NULL, + NULL ); + if ( FAILED( hr ) ) + return false; + + float volumes[MAX_DEVICE_CHANNELS]; + for ( int i = 0; i < MAX_DEVICE_CHANNELS; i++ ) + { + if ( !m_bSurround && i >= 2 ) + { + volumes[i] = 0; + } + else + { + volumes[i] = 1.0; + } + } + m_pSourceVoice->SetChannelVolumes( m_deviceChannels, volumes ); + + m_bufferSizeBytes = XAUDIO2_BUFFER_SAMPLES * (m_deviceSampleBits/8) * m_deviceChannels; + m_pOutputBuffer = new BYTE[MAX_XAUDIO2_BUFFERS * m_bufferSizeBytes]; + ClearBuffer(); + + V_memset( m_Buffers, 0, MAX_XAUDIO2_BUFFERS * sizeof( XAUDIO2_BUFFER ) ); + for ( int i = 0; i < MAX_XAUDIO2_BUFFERS; i++ ) + { + m_Buffers[i].pAudioData = m_pOutputBuffer + i*m_bufferSizeBytes; + m_Buffers[i].pContext = (LPVOID)i; + } + m_BufferHead = 0; + m_BufferTail = 0; + + // number of mono samples output buffer may hold + m_deviceSampleCount = MAX_XAUDIO2_BUFFERS * (m_bufferSizeBytes/(DeviceSampleBytes())); + + // NOTE: This really shouldn't be tied to the # of bufferable samples. + // This just needs to be large enough so that it doesn't fake out the sampling in + // GetSoundTime(). Basically GetSoundTime() assumes a cyclical time stamp and finds wraparound cases + // but that means it needs to get called much more often than once per cycle. So this number should be + // much larger than the framerate in terms of output time + m_clockDivider = m_deviceSampleCount / DeviceChannels(); + + // not really part of XAudio2, but mixer xma lacks one-time init, so doing it here + XMAPlaybackInitialize(); + + hr = m_pSourceVoice->Start( 0 ); + if ( FAILED( hr ) ) + return false; + + DevMsg( "XAudio Device Initialized:\n" ); + DevMsg( " %s\n" + " %d channel(s)\n" + " %d bits/sample\n" + " %d samples/sec\n", DeviceName(), DeviceChannels(), DeviceSampleBits(), DeviceDmaSpeed() ); + + m_VoiceData.VoiceInit(); + + // success + return true; +} + +//----------------------------------------------------------------------------- +// Shutdown XAudio +//----------------------------------------------------------------------------- +void CAudioXAudio::Shutdown( void ) +{ + if ( m_pSourceVoice ) + { + m_pSourceVoice->Stop( 0 ); + m_pSourceVoice->DestroyVoice(); + m_pSourceVoice = NULL; + delete[] m_pOutputBuffer; + } + + if ( m_pMasteringVoice ) + { + m_pMasteringVoice->DestroyVoice(); + m_pMasteringVoice = NULL; + } + + // need to release ref to XAudio2 + m_VoiceData.VoiceShutdown(); + + if ( m_pXAudio2 ) + { + m_pXAudio2->Release(); + m_pXAudio2 = NULL; + } + + if ( this == CAudioXAudio::m_pSingleton ) + { + CAudioXAudio::m_pSingleton = NULL; + } +} + +//----------------------------------------------------------------------------- +// XAudio has completed a packet. Assuming these are sequential +//----------------------------------------------------------------------------- +void CAudioXAudio::XAudioPacketCallback( int hCompletedBuffer ) +{ + // packet completion expected to be sequential + Assert( hCompletedBuffer == (int)( m_PacketTail % MAX_XAUDIO2_BUFFERS ) ); + + m_BufferTail++; + + if ( snd_xaudio_spew_packets.GetBool() ) + { + if ( m_BufferTail == m_BufferHead ) + { + Warning( "XAudio: Starved\n" ); + } + else + { + Msg( "XAudio: Packet Callback, Submit: %2d, Free: %2d\n", m_BufferHead - m_BufferTail, MAX_XAUDIO2_BUFFERS - ( m_BufferHead - m_BufferTail ) ); + } + } + + if ( m_BufferTail == m_BufferHead ) + { + // very bad, out of packets, xaudio is starving + // mix thread didn't keep up with audio clock and submit packets + // submit a silent buffer to keep stream playing and audio clock running + int head = m_BufferHead++; + XAUDIO2_BUFFER *pBuffer = &m_Buffers[head % MAX_XAUDIO2_BUFFERS]; + V_memset( pBuffer->pAudioData, 0, m_bufferSizeBytes ); + pBuffer->AudioBytes = m_bufferSizeBytes; + m_pSourceVoice->SubmitSourceBuffer( pBuffer ); + } +} + +//----------------------------------------------------------------------------- +// Return the "write" cursor. Used to clock the audio mixing. +// The actual hw write cursor and the number of samples it fetches is unknown. +//----------------------------------------------------------------------------- +int CAudioXAudio::GetOutputPosition( void ) +{ + XAUDIO2_VOICE_STATE state; + + state.SamplesPlayed = 0; + m_pSourceVoice->GetState( &state ); + + return ( state.SamplesPlayed % m_clockDivider ); +} + +//----------------------------------------------------------------------------- +// Pause playback +//----------------------------------------------------------------------------- +void CAudioXAudio::Pause( void ) +{ + if ( m_pSourceVoice ) + { + m_pSourceVoice->Stop( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Resume playback +//----------------------------------------------------------------------------- +void CAudioXAudio::UnPause( void ) +{ + if ( m_pSourceVoice ) + { + m_pSourceVoice->Start( 0 ); + } +} + +//----------------------------------------------------------------------------- +// Calc the paint position +//----------------------------------------------------------------------------- +int CAudioXAudio::PaintBegin( float mixAheadTime, int soundtime, int paintedtime ) +{ + // soundtime = total full samples that have been played out to hardware at dmaspeed + // paintedtime = total full samples that have been mixed at speed + + // endtime = target for full samples in mixahead buffer at speed + int mixaheadtime = mixAheadTime * DeviceDmaSpeed(); + int endtime = soundtime + mixaheadtime; + if ( endtime <= paintedtime ) + { + return endtime; + } + + int fullsamps = DeviceSampleCount() / DeviceChannels(); + + if ( ( endtime - soundtime ) > fullsamps ) + { + endtime = soundtime + fullsamps; + } + if ( ( endtime - paintedtime ) & 0x03 ) + { + // The difference between endtime and painted time should align on + // boundaries of 4 samples. This is important when upsampling from 11khz -> 44khz. + endtime -= ( endtime - paintedtime ) & 0x03; + } + + return endtime; +} + +//----------------------------------------------------------------------------- +// Fill the output buffers with silence +//----------------------------------------------------------------------------- +void CAudioXAudio::ClearBuffer( void ) +{ + V_memset( m_pOutputBuffer, 0, MAX_XAUDIO2_BUFFERS * m_bufferSizeBytes ); +} + +//----------------------------------------------------------------------------- +// Fill the output buffer with L/R samples +//----------------------------------------------------------------------------- +int CAudioXAudio::TransferStereo( const portable_samplepair_t *pFrontBuffer, int paintedTime, int endTime, char *pOutputBuffer ) +{ + int linearCount; + int i; + int val; + + int volumeFactor = S_GetMasterVolume() * 256; + + int *pFront = (int *)pFrontBuffer; + short *pOutput = (short *)pOutputBuffer; + + // get size of output buffer in full samples (LR pairs) + // number of sequential sample pairs that can be wrriten + linearCount = g_AudioDevice->DeviceSampleCount() >> 1; + + // clamp output count to requested number of samples + if ( linearCount > endTime - paintedTime ) + { + linearCount = endTime - paintedTime; + } + + // linearCount is now number of mono 16 bit samples (L and R) to xfer. + linearCount <<= 1; + + // transfer mono 16bit samples multiplying each sample by volume. + for ( i=0; i<linearCount; i+=2 ) + { + // L Channel + val = ( pFront[i] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + + // R Channel + val = ( pFront[i+1] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + } + + return linearCount * DeviceSampleBytes(); +} + +//----------------------------------------------------------------------------- +// Fill the output buffer with interleaved surround samples +//----------------------------------------------------------------------------- +int CAudioXAudio::TransferSurroundInterleaved( const portable_samplepair_t *pFrontBuffer, const portable_samplepair_t *pRearBuffer, const portable_samplepair_t *pCenterBuffer, int paintedTime, int endTime, char *pOutputBuffer ) +{ + int linearCount; + int i, j; + int val; + + int volumeFactor = S_GetMasterVolume() * 256; + + int *pFront = (int *)pFrontBuffer; + int *pRear = (int *)pRearBuffer; + int *pCenter = (int *)pCenterBuffer; + short *pOutput = (short *)pOutputBuffer; + + // number of mono samples per channel + // number of sequential samples that can be wrriten + linearCount = m_bufferSizeBytes/( DeviceSampleBytes() * DeviceChannels() ); + + // clamp output count to requested number of samples + if ( linearCount > endTime - paintedTime ) + { + linearCount = endTime - paintedTime; + } + + for ( i = 0, j = 0; i < linearCount; i++, j += 2 ) + { + // FL + val = ( pFront[j] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + + // FR + val = ( pFront[j+1] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + + // Center + val = ( pCenter[j] * volumeFactor) >> 8; + *pOutput++ = CLIP( val ); + + // Let the hardware mix the sub from the main channels since + // we don't have any sub-specific sounds, or direct sub-addressing + *pOutput++ = 0; + + // RL + val = ( pRear[j] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + + // RR + val = ( pRear[j+1] * volumeFactor ) >> 8; + *pOutput++ = CLIP( val ); + } + + return linearCount * DeviceSampleBytes() * DeviceChannels(); +} + +//----------------------------------------------------------------------------- +// Transfer up to a full paintbuffer (PAINTBUFFER_SIZE) of samples out to the xaudio buffer(s). +//----------------------------------------------------------------------------- +void CAudioXAudio::TransferSamples( int endTime ) +{ + XAUDIO2_BUFFER *pBuffer; + + if ( m_BufferHead - m_BufferTail >= MAX_XAUDIO2_BUFFERS ) + { + DevWarning( "XAudio: No Free Buffers!\n" ); + return; + } + + int sampleCount = endTime - g_paintedtime; + if ( sampleCount > XAUDIO2_BUFFER_SAMPLES ) + { + DevWarning( "XAudio: Overflowed mix buffer!\n" ); + endTime = g_paintedtime + XAUDIO2_BUFFER_SAMPLES; + } + + unsigned int nBuffer = m_BufferHead++; + pBuffer = &m_Buffers[nBuffer % MAX_XAUDIO2_BUFFERS]; + + if ( !m_bSurround ) + { + pBuffer->AudioBytes = TransferStereo( PAINTBUFFER, g_paintedtime, endTime, (char *)pBuffer->pAudioData ); + } + else + { + pBuffer->AudioBytes = TransferSurroundInterleaved( PAINTBUFFER, REARPAINTBUFFER, CENTERPAINTBUFFER, g_paintedtime, endTime, (char *)pBuffer->pAudioData ); + } + + // submit buffer + m_pSourceVoice->SubmitSourceBuffer( pBuffer ); +} + +//----------------------------------------------------------------------------- +// Get our device name +//----------------------------------------------------------------------------- +const char *CAudioXAudio::DeviceName( void ) +{ + if ( m_bSurround ) + { + return "XAudio: 5.1 Channel Surround"; + } + + return "XAudio: Stereo"; +} + +CXboxVoice::CXboxVoice() +{ + m_pXHVEngine = NULL; +} + +//----------------------------------------------------------------------------- +// Initialize Voice +//----------------------------------------------------------------------------- +void CXboxVoice::VoiceInit( void ) +{ + if ( !m_pXHVEngine ) + { + // Set the processing modes + XHV_PROCESSING_MODE rgMode = XHV_VOICECHAT_MODE; + + // Set up parameters for the voice chat engine + XHV_INIT_PARAMS xhvParams = {0}; + xhvParams.dwMaxRemoteTalkers = MAX_PLAYERS; + xhvParams.dwMaxLocalTalkers = XUSER_MAX_COUNT; + xhvParams.localTalkerEnabledModes = &rgMode; + xhvParams.remoteTalkerEnabledModes = &rgMode; + xhvParams.dwNumLocalTalkerEnabledModes = 1; + xhvParams.dwNumRemoteTalkerEnabledModes = 1; + xhvParams.pXAudio2 = CAudioXAudio::m_pSingleton->GetXAudio2(); + + // Create the engine + HRESULT hr = XHV2CreateEngine( &xhvParams, NULL, &m_pXHVEngine ); + if ( hr != S_OK ) + { + Warning( "Couldn't load XHV engine!\n" ); + } + } + + VoiceResetLocalData( ); +} + +void CXboxVoice::VoiceShutdown( void ) +{ + if ( !m_pXHVEngine ) + return; + + m_pXHVEngine->Release(); + m_pXHVEngine = NULL; +} + +void CXboxVoice::AddPlayerToVoiceList( CClientInfo *pClient, bool bLocal ) +{ + XHV_PROCESSING_MODE local_proc_mode = XHV_VOICECHAT_MODE; + + for ( int i = 0; i < pClient->m_cPlayers; ++i ) + { + if ( pClient->m_xuids[i] == 0 ) + continue; + + if ( bLocal == true ) + { + if ( m_pXHVEngine->RegisterLocalTalker( pClient->m_iControllers[i] ) == S_OK ) + { + m_pXHVEngine->StartLocalProcessingModes( pClient->m_iControllers[i], &local_proc_mode, 1 ); + } + } + else + { + if ( m_pXHVEngine->RegisterRemoteTalker( pClient->m_xuids[i], NULL, NULL, NULL ) == S_OK ) + { + m_pXHVEngine->StartRemoteProcessingModes( pClient->m_xuids[i], &local_proc_mode, 1 ); + } + } + } +} + +void CXboxVoice::RemovePlayerFromVoiceList( CClientInfo *pClient, bool bLocal ) +{ + for ( int i = 0; i < pClient->m_cPlayers; ++i ) + { + if ( pClient->m_xuids[i] == 0 ) + continue; + + if ( bLocal == true ) + { + m_pXHVEngine->UnregisterLocalTalker( pClient->m_iControllers[i] ); + } + else + { + m_pXHVEngine->UnregisterRemoteTalker( pClient->m_xuids[i] ); + } + } +} + +void CXboxVoice::PlayIncomingVoiceData( XUID xuid, const byte *pbData, DWORD pdwSize ) +{ + XUID localXUID; + + XUserGetXUID( XBX_GetPrimaryUserId(), &localXUID ); + + //Hack: Don't play stuff that comes from ourselves. + if ( localXUID == xuid ) + return; + + m_pXHVEngine->SubmitIncomingChatData( xuid, pbData, &pdwSize ); +} + + +void CXboxVoice::UpdateHUDVoiceStatus( void ) +{ + for ( int iClient = 0; iClient < cl.m_nMaxClients; iClient++ ) + { + bool bSelf = (cl.m_nPlayerSlot == iClient); + + int iIndex = iClient + 1; + XUID id = g_pMatchmaking->PlayerIdToXuid( iIndex ); + + if ( id != 0 ) + { + bool bTalking = false; + + if ( bSelf == true ) + { + //Make sure the player's own label is not on. + g_pSoundServices->OnChangeVoiceStatus( iIndex, false ); + + iIndex = -1; + + if ( IsPlayerTalking( XBX_GetPrimaryUserId(), true ) ) + { + bTalking = true; + } + } + else + { + if ( IsPlayerTalking( id, false ) ) + { + bTalking = true; + } + } + + g_pSoundServices->OnChangeVoiceStatus( iIndex, bTalking ); + } + else + { + g_pSoundServices->OnChangeVoiceStatus( iIndex, false ); + } + } +} + +bool CXboxVoice::VoiceUpdateData( void ) +{ + DWORD dwNumPackets = 0; + DWORD dwBytes = 0; + WORD wVoiceBytes = 0; + bool bShouldSend = false; + DWORD dwVoiceFlags = m_pXHVEngine->GetDataReadyFlags(); + + //Update UI stuff. + UpdateHUDVoiceStatus(); + + for ( uint i = 0; i < XUSER_MAX_COUNT; ++i ) + { + // We currently only allow one player per console + if ( i != XBX_GetPrimaryUserId() ) + { + continue; + } + + if ( IsHeadsetPresent( i ) == false ) + continue; + + if ( !(dwVoiceFlags & ( 1 << i )) ) + continue; + + dwBytes = m_ChatBufferSize - m_wLocalDataSize; + + if( dwBytes < XHV_VOICECHAT_MODE_PACKET_SIZE ) + { + bShouldSend = true; + } + else + { + m_pXHVEngine->GetLocalChatData( i, m_ChatBuffer + m_wLocalDataSize, &dwBytes, &dwNumPackets ); + m_wLocalDataSize += ((WORD)dwBytes) & MAXWORD; + + if( m_wLocalDataSize > ( ( m_ChatBufferSize * 7 ) / 10 ) ) + { + bShouldSend = true; + } + } + + wVoiceBytes += m_wLocalDataSize & MAXWORD; + break; + } + + return bShouldSend || + ( wVoiceBytes && + ( GetTickCount() - m_dwLastVoiceSend ) > MAX_VOICE_BUFFER_TIME ); +} + +void CXboxVoice::SetPlaybackPriority( XUID remoteTalker, DWORD dwUserIndex, XHV_PLAYBACK_PRIORITY playbackPriority ) +{ + m_pXHVEngine->SetPlaybackPriority( remoteTalker, dwUserIndex, playbackPriority ); +} + +void CXboxVoice::GetRemoteTalkers( int *pNumTalkers, XUID *pRemoteTalkers ) +{ + m_pXHVEngine->GetRemoteTalkers( (DWORD*)pNumTalkers, pRemoteTalkers ); +} + +void CXboxVoice::GetVoiceData( CLC_VoiceData *pMessage ) +{ + byte *puchVoiceData = NULL; + pMessage->m_nLength = m_wLocalDataSize; + XUserGetXUID( XBX_GetPrimaryUserId(), &pMessage->m_xuid ); + + puchVoiceData = m_ChatBuffer; + + pMessage->m_DataOut.StartWriting( puchVoiceData, pMessage->m_nLength ); + pMessage->m_nLength *= 8; + pMessage->m_DataOut.SeekToBit( pMessage->m_nLength ); // set correct writing position +} + +void CXboxVoice::VoiceSendData( INetChannel *pChannel ) +{ + CLC_VoiceData voiceMsg; + GetVoiceData( &voiceMsg ); + + if ( pChannel ) + { + pChannel->SendNetMsg( voiceMsg, false, true ); + VoiceResetLocalData(); + } +} + +void CXboxVoice::VoiceResetLocalData( void ) +{ + m_wLocalDataSize = 0; + m_dwLastVoiceSend = GetTickCount(); + Q_memset( m_ChatBuffer, 0, m_ChatBufferSize ); +} + +bool CXboxVoice::IsPlayerTalking( XUID uid, bool bLocal ) +{ + if ( bLocal == true ) + { + return m_pXHVEngine->IsLocalTalking( XBX_GetPrimaryUserId() ); + } + else + { + return !g_pMatchmaking->IsPlayerMuted( XBX_GetPrimaryUserId(), uid ) && m_pXHVEngine->IsRemoteTalking( uid ); + } + + return false; +} + +bool CXboxVoice::IsHeadsetPresent( int id ) +{ + return m_pXHVEngine->IsHeadsetPresent( id ); +} + +void CXboxVoice::RemoveAllTalkers( CClientInfo *pLocal ) +{ + int numRemoteTalkers; + XUID remoteTalkers[MAX_PLAYERS]; + GetRemoteTalkers( &numRemoteTalkers, remoteTalkers ); + + for ( int iRemote = 0; iRemote < numRemoteTalkers; iRemote++ ) + { + m_pXHVEngine->UnregisterRemoteTalker( remoteTalkers[iRemote] ); + } + + if ( pLocal ) + { + for ( int i = 0; i < pLocal->m_cPlayers; ++i ) + { + if ( pLocal->m_xuids[i] == 0 ) + continue; + + m_pXHVEngine->UnregisterLocalTalker( pLocal->m_iControllers[i] ); + } + } +}
\ No newline at end of file diff --git a/engine/audio/private/snd_dev_xaudio.h b/engine/audio/private/snd_dev_xaudio.h new file mode 100644 index 0000000..83ec00b --- /dev/null +++ b/engine/audio/private/snd_dev_xaudio.h @@ -0,0 +1,64 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_DEV_XAUDIO_H +#define SND_DEV_XAUDIO_H +#pragma once +#include "audio_pch.h" +#include "inetmessage.h" +#include "netmessages.h" + +class IAudioDevice; +IAudioDevice *Audio_CreateXAudioDevice( void ); + +#if defined ( _X360 ) + +class CClientInfo; +void VOICE_AddPlayerToVoiceList( CClientInfo *pClient, bool bLocal ); + +class CXboxVoice +{ +public: + + static const DWORD MAX_VOICE_BUFFER_TIME = 200; // 200ms + CXboxVoice( void ); + void VoiceInit( void ); + void VoiceShutdown( void ); + void AddPlayerToVoiceList( CClientInfo *pClient, bool bLocal ); + void RemovePlayerFromVoiceList( CClientInfo *pClient, bool bLocal ); + bool VoiceUpdateData( void ); + void GetVoiceData( CLC_VoiceData *pData ); + void VoiceSendData( INetChannel *pChannel ); + void VoiceResetLocalData( void ); + void PlayIncomingVoiceData( XUID xuid, const byte *pbData, DWORD pdwSize ); + void UpdateHUDVoiceStatus( void ); + void GetRemoteTalkers( int *pNumTalkers, XUID *pRemoteTalkers ); + void SetPlaybackPriority( XUID remoteTalker, DWORD dwUserIndex, XHV_PLAYBACK_PRIORITY playbackPriority ); + bool IsPlayerTalking( XUID uid, bool bLocal ); + bool IsHeadsetPresent( int id ); + void RemoveAllTalkers( CClientInfo *pLocal ); + +private: + PIXHV2ENGINE m_pXHVEngine; + + + // Local chat data + static const WORD m_ChatBufferSize = XHV_VOICECHAT_MODE_PACKET_SIZE * XHV_MAX_VOICECHAT_PACKETS; + BYTE m_ChatBuffer[ m_ChatBufferSize ]; + WORD m_wLocalDataSize; + + // Last voice data sent + DWORD m_dwLastVoiceSend; +}; + +CXboxVoice *Audio_GetXVoice( void ); +IXAudio2 *Audio_GetXAudio2( void ); + +#endif + + + +#endif // SND_DEV_XAUDIO_H diff --git a/engine/audio/private/snd_dma.cpp b/engine/audio/private/snd_dma.cpp new file mode 100644 index 0000000..137bce7 --- /dev/null +++ b/engine/audio/private/snd_dma.cpp @@ -0,0 +1,8401 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Main control for any streaming sound output device. +// +//===========================================================================// + +#include "audio_pch.h" +#include "const.h" +#include "cdll_int.h" +#include "client_class.h" +#include "icliententitylist.h" +#include "tier0/vcrmode.h" +#include "con_nprint.h" +#include "tier0/icommandline.h" +#include "vox_private.h" +#include "../../traceinit.h" +#include "../../cmd.h" +#include "toolframework/itoolframework.h" +#include "vstdlib/random.h" +#include "vstdlib/jobthread.h" +#include "vaudio/ivaudio.h" +#include "../../client.h" +#include "../../cl_main.h" +#include "utldict.h" +#include "mempool.h" +#include "../../enginetrace.h" // for traceline +#include "../../public/bspflags.h" // for traceline +#include "../../public/gametrace.h" // for traceline +#include "vphysics_interface.h" // for surface props +#include "../../ispatialpartitioninternal.h" // for entity enumerator +#include "../../debugoverlay.h" +#include "icliententity.h" +#include "../../cmodel_engine.h" +#include "../../staticpropmgr.h" +#include "../../server.h" +#include "edict.h" +#include "../../pure_server.h" +#include "filesystem/IQueuedLoader.h" +#include "voice.h" +#if defined( _X360 ) +#include "xbox/xbox_console.h" +#include "xmp.h" +#endif + +#include "replay/iclientreplaycontext.h" +#include "replay/ireplaymovierenderer.h" + +#include "video/ivideoservices.h" +extern IVideoServices *g_pVideo; + +/* +#include "gl_model_private.h" +#include "world.h" +#include "vphysics_interface.h" +#include "client_class.h" +#include "server_class.h" +*/ + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +/////////////////////////////////// +// DEBUGGING +// +// Turn this on to print channel output msgs. +// +//#define DEBUG_CHANNELS + +#define SNDLVL_TO_DIST_MULT( sndlvl ) ( sndlvl ? ((pow( 10.0f, snd_refdb.GetFloat() / 20 ) / pow( 10.0f, (float)sndlvl / 20 )) / snd_refdist.GetFloat()) : 0 ) +#define DIST_MULT_TO_SNDLVL( dist_mult ) (soundlevel_t)(int)( dist_mult ? ( 20 * log10( pow( 10.0f, snd_refdb.GetFloat() / 20 ) / (dist_mult * snd_refdist.GetFloat()) ) ) : 0 ) + +extern ConVar dsp_spatial; +extern IPhysicsSurfaceProps *physprop; + +extern bool IsReplayRendering(); + +static void S_Play( const CCommand &args ); +static void S_PlayVol( const CCommand &args ); +void S_SoundList(void); +static void S_Say ( const CCommand &args ); +void S_Update_(float); +void S_StopAllSounds(bool clear); +void S_StopAllSoundsC(void); +void S_ShutdownMixThread(); +const char *GetClientClassname( SoundSource soundsource ); + +float SND_GetGainObscured( channel_t *ch, bool fplayersound, bool flooping, bool bAttenuated ); +void DSP_ChangePresetValue( int idsp, int channel, int iproc, float value ); +bool DSP_CheckDspAutoEnabled( void ); +void DSP_SetDspAuto( int dsp_preset ); +float dB_To_Radius ( float db ); +int dsp_room_GetInt ( void ); + +bool MXR_LoadAllSoundMixers( void ); +void MXR_ReleaseMemory( void ); +int MXR_GetMixGroupListFromDirName( const char *pDirname, byte *pList, int listMax ); +void MXR_GetMixGroupFromSoundsource( channel_t *pchan, SoundSource soundsource, soundlevel_t soundlevel); +float MXR_GetVolFromMixGroup( int rgmixgroupid[8], int *plast_mixgroupid ); +char *MXR_GetGroupnameFromId( int mixgroupid ); +int MXR_GetMixgroupFromName( const char *pszgroupname ); +void MXR_DebugShowMixVolumes( void ); +#ifdef _DEBUG +static void MXR_DebugSetMixGroupVolume( const CCommand &args ); +#endif //_DEBUG +void MXR_UpdateAllDuckerVolumes( void ); + +void ChannelSetVolTargets( channel_t *pch, int *pvolumes, int ivol_offset, int cvol ); +void ChannelUpdateVolXfade( channel_t *pch ); +void ChannelClearVolumes( channel_t *pch ); +float VOX_GetChanVol(channel_t *ch); +void ConvertListenerVectorTo2D( Vector *pvforward, Vector *pvright ); +int ChannelGetMaxVol( channel_t *pch ); + +// Forceably ends voice tweak mode (only occurs during snd_restart +void VoiceTweak_EndVoiceTweakMode(); +bool VoiceTweak_IsStillTweaking(); +// Only does anything for voice tweak channel so if view entity changes it doesn't fade out to zero volume +void Voice_Spatialize( channel_t *channel ); + +// ======================================================================= +// Internal sound data & structures +// ======================================================================= + +channel_t channels[MAX_CHANNELS]; + +int total_channels; +CActiveChannels g_ActiveChannels; +static double g_LastSoundFrame = 0.0f; // last full frame of sound +static double g_LastMixTime = 0.0f; // last time we did mixing +static float g_EstFrameTime = 0.1f; // estimated frame time running average + +// x360 override to fade out game music when the user is playing music through the dashboard +static float g_DashboardMusicMixValue = 1.0f; +static float g_DashboardMusicMixTarget = 1.0f; +const float g_DashboardMusicFadeRate = 0.5f; // Fades one half full-scale volume per second (two seconds for complete fadeout) + +// sound mixers +int g_csoundmixers = 0; // total number of soundmixers found +int g_cgrouprules = 0; // total number of group rules found +int g_cgroupclass = 0; + +// this is used to enable/disable music playback on x360 when the user selects his own soundtrack to play +void S_EnableMusic( bool bEnable ) +{ + if ( bEnable ) + { + g_DashboardMusicMixTarget = 1.0f; + } + else + { + g_DashboardMusicMixTarget = 0.0f; + } +} + +bool IsSoundSourceLocalPlayer( int soundsource ) +{ + if ( soundsource == SOUND_FROM_UI_PANEL ) + return true; + + return ( soundsource == g_pSoundServices->GetViewEntity() ); +} + +CThreadMutex g_SndMutex; + +#define THREAD_LOCK_SOUND() AUTO_LOCK( g_SndMutex ) + +const int MASK_BLOCK_AUDIO = CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_WINDOW; + +void CActiveChannels::Add( channel_t *pChannel ) +{ + Assert( pChannel->activeIndex == 0 ); + m_list[m_count] = pChannel - channels; + m_count++; + pChannel->activeIndex = m_count; +} + +void CActiveChannels::Remove( channel_t *pChannel ) +{ + if ( pChannel->activeIndex == 0 ) + return; + int activeIndex = pChannel->activeIndex - 1; + Assert( activeIndex >= 0 && activeIndex < m_count ); + Assert( pChannel == &channels[m_list[activeIndex]] ); + m_count--; + // Not the last one? Swap the last one with this one and fix its index + if ( activeIndex < m_count ) + { + m_list[activeIndex] = m_list[m_count]; + channels[m_list[activeIndex]].activeIndex = activeIndex+1; + } + pChannel->activeIndex = 0; +} + + +void CActiveChannels::GetActiveChannels( CChannelList &list ) +{ + list.m_count = m_count; + if ( m_count ) + { + Q_memcpy( list.m_list, m_list, sizeof(m_list[0])*m_count ); + } + + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + if ( pSpecialBuffer->nSpecialDSP != 0 ) + { + list.m_nSpecialDSPs.AddToTail( pSpecialBuffer->nSpecialDSP ); + } + } + + list.m_hasSpeakerChannels = true; + list.m_has11kChannels = true; + list.m_has22kChannels = true; + list.m_has44kChannels = true; + list.m_hasDryChannels = true; +} + +void CActiveChannels::Init() +{ + m_count = 0; +} + +bool snd_initialized = false; + +Vector listener_origin; +static Vector listener_forward; +Vector listener_right; +static Vector listener_up; +static bool s_bIsListenerUnderwater; +static vec_t sound_nominal_clip_dist=SOUND_NORMAL_CLIP_DIST; + +// @TODO (toml 05-08-02): put this somewhere more reasonable +vec_t S_GetNominalClipDist() +{ + return sound_nominal_clip_dist; +} + +int g_soundtime = 0; // sample PAIRS output since start +int g_paintedtime = 0; // sample PAIRS mixed since start + +float g_ReplaySoundTimeFracAccumulator = 0.0f; // Used by replay + +float g_ClockSyncArray[NUM_CLOCK_SYNCS] = {0}; +int g_SoundClockPaintTime[NUM_CLOCK_SYNCS] = {0}; + +// default 10ms +ConVar snd_delay_sound_shift("snd_delay_sound_shift","0.01"); +// this forces the clock to resync on the next delayed/sync sound +void S_SyncClockAdjust( clocksync_index_t syncIndex ) +{ + g_ClockSyncArray[syncIndex] = 0; + g_SoundClockPaintTime[syncIndex] = 0; +} + +float S_ComputeDelayForSoundtime( float soundtime, clocksync_index_t syncIndex ) +{ + // reset clock and return 0 + if ( g_ClockSyncArray[syncIndex] == 0 ) + { + // Put the current time marker one tick back to impose a minimum delay on the first sample + // this shifts the drift over so the sounds are more likely to delay (rather than skip) + // over the burst + // NOTE: The first sound after a sync MUST have a non-zero delay for the delay channel + // detection logic to work (otherwise we keep resetting the clock) + g_ClockSyncArray[syncIndex] = soundtime - host_state.interval_per_tick; + g_SoundClockPaintTime[syncIndex] = g_paintedtime; + } + + // how much time has passed in the game since we did a clock sync? + float gameDeltaTime = soundtime - g_ClockSyncArray[syncIndex]; + + // how many samples have been mixed since we did a clock sync? + int paintedSamples = g_paintedtime - g_SoundClockPaintTime[syncIndex]; + int dmaSpeed = g_AudioDevice->DeviceDmaSpeed(); + int gameSamples = (gameDeltaTime * dmaSpeed); + int delaySamples = gameSamples - paintedSamples; + float delay = delaySamples / float(dmaSpeed); + + if ( gameDeltaTime < 0 || fabs(delay) > 0.500f ) + { + // Note that the equations assume a correlation between game time and real time + // some kind of clock error. This can happen with large host_timescale or when the + // framerate hitches drastically (game time is a smaller clamped value wrt real time). + // The current sync estimate has probably drifted due to this or some other problem, recompute. + //Msg("Clock ERROR!: %.2f %.2f\n", gameDeltaTime, delay); + S_SyncClockAdjust(syncIndex); + return 0; + } + return delay + snd_delay_sound_shift.GetFloat(); +} + +static int s_buffers = 0; +static int s_oldsampleOutCount = 0; +static float s_lastsoundtime = 0.0f; + +bool s_bOnLoadScreen = false; + +static CClassMemoryPool< CSfxTable > s_SoundPool( MAX_SFX ); +struct SfxDictEntry +{ + CSfxTable *pSfx; +}; + +static CUtlMap< FileNameHandle_t, SfxDictEntry > s_Sounds( 0, 0, DefLessFunc( FileNameHandle_t ) ); + +class CDummySfx : public CSfxTable +{ +public: + virtual const char *getname() + { + return name; + } + + void setname( const char *pName ) + { + Q_strncpy( name, pName, sizeof( name ) ); + OnNameChanged(name); + } + +private: + char name[MAX_PATH]; +}; + +static CDummySfx dummySfx; + +// returns true if ok to procede with TraceRay calls +bool SND_IsInGame( void ) +{ + return cl.IsActive(); +} + + +CSfxTable::CSfxTable() +{ + m_namePoolIndex = s_Sounds.InvalidIndex(); + pSource = NULL; + m_bUseErrorFilename = false; + m_bIsUISound = false; + m_bIsLateLoad = false; + m_bMixGroupsCached = false; + m_pDebugName = NULL; +} + + +void CSfxTable::SetNamePoolIndex( int index ) +{ + m_namePoolIndex = index; + if ( m_namePoolIndex != s_Sounds.InvalidIndex() ) + { + OnNameChanged(getname()); + } +#ifdef _DEBUG + m_pDebugName = strdup( getname() ); +#endif +} + +void CSfxTable::OnNameChanged( const char *pName ) +{ + if ( pName && g_cgrouprules ) + { + char szString[MAX_PATH]; + Q_strncpy( szString, pName, sizeof(szString) ); + Q_FixSlashes( szString, '/' ); + m_mixGroupCount = MXR_GetMixGroupListFromDirName( szString, m_mixGroupList, ARRAYSIZE(m_mixGroupList) ); + m_bMixGroupsCached = true; + } +} +//----------------------------------------------------------------------------- +// Purpose: Wrapper for sfxtable->getname() +// Output : char const +//----------------------------------------------------------------------------- +const char *CSfxTable::getname() +{ + if ( s_Sounds.InvalidIndex() != m_namePoolIndex ) + { + char* pString = tmpstr512(); + if ( g_pFileSystem ) + g_pFileSystem->String( s_Sounds.Key( m_namePoolIndex ), pString, 512 ); + else + { + pString[0] = 0; + } + return pString; + } + + return NULL; +} + +FileNameHandle_t CSfxTable::GetFileNameHandle() +{ + if ( s_Sounds.InvalidIndex() != m_namePoolIndex ) + { + return s_Sounds.Key( m_namePoolIndex ); + } + return NULL; +} + +const char *CSfxTable::GetFileName() +{ + if ( IsX360() && m_bUseErrorFilename ) + { + // Redirecting error sounds to a valid empty wave, prevents a bad loading retry pattern during gameplay + // which may event sounds skipped by preload, because they don't exist. + return "common/null.wav"; + } + + const char *pName = getname(); + return pName ? PSkipSoundChars( pName ) : NULL; +} + +bool CSfxTable::IsPrecachedSound() +{ + const char *pName = getname(); + + if ( sv.IsActive() ) + { + // Server uses zero to mark invalid sounds + return sv.LookupSoundIndex( pName ) != 0 ? true : false; + } + + // Client uses -1 + // WE SHOULD FIX THIS!!! + return ( cl.LookupSoundIndex( pName ) != -1 ) ? true : false; +} + +float g_DuckScale = 1.0f; + +// Structure used for fading in and out client sound volume. +typedef struct +{ + float initial_percent; + + // How far to adjust client's volume down by. + float percent; + + // GetHostTime() when we started adjusting volume + float starttime; + + // # of seconds to get to faded out state + float fadeouttime; + // # of seconds to hold + float holdtime; + // # of seconds to restore + float fadeintime; +} soundfade_t; + +static soundfade_t soundfade; // Client sound fading singleton object + +// 0)headphones 2)stereo speakers 4)quad 5)5point1 +// autodetected from windows settings +ConVar snd_surround( "snd_surround_speakers", "-1", FCVAR_INTERNAL_USE ); +ConVar snd_legacy_surround( "snd_legacy_surround", "0", FCVAR_ARCHIVE ); +ConVar snd_noextraupdate( "snd_noextraupdate", "0" ); +ConVar snd_show( "snd_show", "0", FCVAR_CHEAT, "Show sounds info" ); +ConVar snd_visualize ("snd_visualize", "0", FCVAR_CHEAT, "Show sounds location in world" ); +ConVar snd_pitchquality( "snd_pitchquality", "1", FCVAR_ARCHIVE ); // 1) use high quality pitch shifters + +// master volume +static ConVar volume( "volume", "1.0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Sound volume", true, 0.0f, true, 1.0f ); +// user configurable music volume +ConVar snd_musicvolume( "snd_musicvolume", "1.0", FCVAR_ARCHIVE | FCVAR_ARCHIVE_XBOX, "Music volume", true, 0.0f, true, 1.0f ); + +ConVar snd_mixahead( "snd_mixahead", "0.1", FCVAR_ARCHIVE ); +ConVar snd_mix_async( "snd_mix_async", "0" ); +#ifdef _DEBUG +static ConCommand snd_mixvol("snd_mixvol", MXR_DebugSetMixGroupVolume, "Set named Mixgroup to mix volume."); +#endif + +// vaudio DLL +IVAudio *vaudio = NULL; +CSysModule *g_pVAudioModule = NULL; + +//----------------------------------------------------------------------------- +// Resource loading for sound +//----------------------------------------------------------------------------- +class CResourcePreloadSound : public CResourcePreload +{ +public: + CResourcePreloadSound() : m_SoundNames( 0, 0, true ) + { + } + + virtual bool CreateResource( const char *pName ) + { + CSfxTable *pSfx = S_PrecacheSound( pName ); + if ( !pSfx ) + { + return false; + } + + m_SoundNames.AddString( pSfx->GetFileName() ); + return true; + } + + virtual void PurgeUnreferencedResources() + { + bool bSpew = ( g_pQueuedLoader->GetSpewDetail() & LOADER_DETAIL_PURGES ) != 0; + + for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) + { + // the master sound table grows forever + // remove sound sources from the master sound table that were not in the preload list + CSfxTable *pSfx = s_Sounds[i].pSfx; + if ( pSfx && pSfx->pSource ) + { + if ( pSfx->m_bIsUISound ) + { + // never purge ui + continue; + } + + UtlSymId_t symbol = m_SoundNames.Find( pSfx->GetFileName() ); + if ( symbol == UTL_INVAL_SYMBOL ) + { + // sound was not part of preload, purge it + if ( bSpew ) + { + Msg( "CResourcePreloadSound: Purging: %s\n", pSfx->GetFileName() ); + } + + pSfx->pSource->CacheUnload(); + delete pSfx->pSource; + pSfx->pSource = NULL; + } + } + } + + m_SoundNames.RemoveAll(); + + if ( !g_pQueuedLoader->IsSameMapLoading() ) + { + wavedatacache->Flush(); + } + } + + virtual void PurgeAll() + { + bool bSpew = ( g_pQueuedLoader->GetSpewDetail() & LOADER_DETAIL_PURGES ) != 0; + + for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) + { + // the master sound table grows forever + // remove sound sources from the master sound table that were not in the preload list + CSfxTable *pSfx = s_Sounds[i].pSfx; + if ( pSfx && pSfx->pSource ) + { + if ( pSfx->m_bIsUISound ) + { + // never purge ui + if ( bSpew ) + { + Msg( "CResourcePreloadSound: Skipping: %s\n", pSfx->GetFileName() ); + } + continue; + } + + // sound was not part of preload, purge it + if ( bSpew ) + { + Msg( "CResourcePreloadSound: Purging: %s\n", pSfx->GetFileName() ); + } + + pSfx->pSource->CacheUnload(); + delete pSfx->pSource; + pSfx->pSource = NULL; + } + } + + m_SoundNames.RemoveAll(); + + wavedatacache->Flush(); + } + +private: + CUtlSymbolTable m_SoundNames; +}; +static CResourcePreloadSound s_ResourcePreloadSound; + +//----------------------------------------------------------------------------- +// Purpose: +// Output : float +//----------------------------------------------------------------------------- +float S_GetMasterVolume( void ) +{ + float scale = 1.0f; + if ( soundfade.percent != 0 ) + { + scale = clamp( (float)soundfade.percent / 100.0f, 0.0f, 1.0f ); + scale = 1.0f - scale; + } + return volume.GetFloat() * scale; +} + + +void S_SoundInfo_f(void) +{ + if ( !g_AudioDevice->IsActive() ) + { + Msg( "Sound system not started\n" ); + return; + } + + Msg( "Sound Device: %s\n", g_AudioDevice->DeviceName() ); + Msg( " Channels: %d\n", g_AudioDevice->DeviceChannels() ); + Msg( " Samples: %d\n", g_AudioDevice->DeviceSampleCount() ); + Msg( " Bits/Sample: %d\n", g_AudioDevice->DeviceSampleBits() ); + Msg( " Rate: %d\n", g_AudioDevice->DeviceDmaSpeed() ); + Msg( "total_channels: %d\n", total_channels); + + if ( IsX360() ) + { + // dump a glimpse of the mixing state + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + + Msg( "\nActive Channels: (%d)\n", list.Count() ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = list.GetChannel(i); + Msg( "%s (Mixer: 0x%p)\n", pChannel->sfx->GetFileName(), pChannel->pMixer ); + } + } +} + + +/* +================ +S_Startup +================ +*/ + +void S_Startup( void ) +{ + if ( !snd_initialized ) + return; + + if ( !g_AudioDevice || g_AudioDevice == Audio_GetNullDevice() ) + { + g_AudioDevice = IAudioDevice::AutoDetectInit( CommandLine()->CheckParm( "-wavonly" ) != 0 ); + if ( !g_AudioDevice ) + { + Error( "Unable to init audio" ); + } + } +} + +static ConCommand play("play", S_Play, "Play a sound.", FCVAR_SERVER_CAN_EXECUTE ); +static ConCommand playflush( "playflush", S_Play, "Play a sound, reloading from disk in case of changes." ); +static ConCommand playvol( "playvol", S_PlayVol, "Play a sound at a specified volume." ); +static ConCommand speak( "speak", S_Say, "Play a constructed sentence." ); +static ConCommand stopsound( "stopsound", S_StopAllSoundsC, 0, FCVAR_CHEAT); // Marked cheat because it gives an advantage to players minimising ambient noise. +static ConCommand soundlist( "soundlist", S_SoundList, "List all known sounds." ); +static ConCommand soundinfo( "soundinfo", S_SoundInfo_f, "Describe the current sound device." ); + +bool IsValidSampleRate( int rate ) +{ + return rate == SOUND_11k || rate == SOUND_22k || rate == SOUND_44k; +} + +void VAudioInit() +{ + if ( IsPC() ) + { + if ( !IsPosix() ) + { + // vaudio_miles.dll will load this... + g_pFileSystem->GetLocalCopy( "mss32.dll" ); + } + + g_pVAudioModule = FileSystem_LoadModule( "vaudio_miles" ); + if ( g_pVAudioModule ) + { + CreateInterfaceFn vaudioFactory = Sys_GetFactory( g_pVAudioModule ); + vaudio = (IVAudio *)vaudioFactory( VAUDIO_INTERFACE_VERSION, NULL ); + } + } +} + +/* +================ +S_Init +================ +*/ +void S_Init( void ) +{ + if ( sv.IsDedicated() && !CommandLine()->CheckParm( "-forcesound" ) ) + return; + + DevMsg( "Sound Initialization: Start\n" ); + + // KDB: init sentence array + TRACEINIT( VOX_Init(), VOX_Shutdown() ); + + VAudioInit(); + + if ( CommandLine()->CheckParm( "-nosound" ) ) + { + g_AudioDevice = Audio_GetNullDevice(); + TRACEINIT( audiosourcecache->Init( host_parms.memsize >> 2 ), audiosourcecache->Shutdown() ); + return; + } + + snd_initialized = true; + + g_ActiveChannels.Init(); + S_Startup(); + + MIX_InitAllPaintbuffers(); + + SND_InitScaletable(); + + MXR_LoadAllSoundMixers(); + + S_StopAllSounds( true ); + + TRACEINIT( audiosourcecache->Init( host_parms.memsize >> 2 ), audiosourcecache->Shutdown() ); + + AllocDsps( true ); + + if ( IsX360() ) + { + g_pQueuedLoader->InstallLoader( RESOURCEPRELOAD_SOUND, &s_ResourcePreloadSound ); + } + + DevMsg( "Sound Initialization: Finish, Sampling Rate: %i\n", g_AudioDevice->DeviceDmaSpeed() ); + +#ifdef _X360 + BOOL bPlaybackControl; + // get initial state of the x360 media player + if ( XMPTitleHasPlaybackControl( &bPlaybackControl ) == ERROR_SUCCESS ) + { + S_EnableMusic(bPlaybackControl!=0); + } + + Assert( g_pVideo != NULL ); + + if ( g_pVideo != NULL ) + { + if ( g_pVideo->SoundDeviceCommand( VideoSoundDeviceOperation::HOOK_X_AUDIO, NULL ) != VideoResult::SUCCESS ) + { + Assert( 0 ); + } + } +#endif +} + + +// ======================================================================= +// Shutdown sound engine +// ======================================================================= +void S_Shutdown(void) +{ +#if !defined( _X360 ) + if ( VoiceTweak_IsStillTweaking() ) + { + VoiceTweak_EndVoiceTweakMode(); + } +#endif + + S_StopAllSounds( true ); + S_ShutdownMixThread(); + + TRACESHUTDOWN( audiosourcecache->Shutdown() ); + + SNDDMA_Shutdown(); + + for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) + { + if ( s_Sounds[i].pSfx ) + { + delete s_Sounds[i].pSfx->pSource; + s_Sounds[i].pSfx->pSource = NULL; + } + } + s_Sounds.RemoveAll(); + s_SoundPool.Clear(); + + // release DSP resources + FreeDsps( true ); + + MXR_ReleaseMemory(); + + // release sentences resources + TRACESHUTDOWN( VOX_Shutdown() ); + + if ( IsPC() ) + { + // shutdown vaudio + if ( vaudio ) + delete vaudio; + FileSystem_UnloadModule( g_pVAudioModule ); + g_pVAudioModule = NULL; + vaudio = NULL; + } + + MIX_FreeAllPaintbuffers(); + snd_initialized = false; + g_paintedtime = 0; + g_soundtime = 0; + g_ReplaySoundTimeFracAccumulator = 0.0f; + s_buffers = 0; + s_oldsampleOutCount = 0; + s_lastsoundtime = 0.0f; +#if !defined( _X360 ) + Voice_Deinit(); +#endif +} + +bool S_IsInitted() +{ + return snd_initialized; +} + +// ======================================================================= +// Load a sound +// ======================================================================= + +//----------------------------------------------------------------------------- +// Return sfx and set pfInCache to 1 if +// name is in name cache. Otherwise, alloc +// a new spot in name cache and return 0 +// in pfInCache. +//----------------------------------------------------------------------------- +CSfxTable *S_FindName( const char *szName, int *pfInCache ) +{ + int i; + CSfxTable *sfx = NULL; + char szBuff[MAX_PATH]; + const char *pName; + + if ( !szName ) + { + Error( "S_FindName: NULL\n" ); + } + + pName = szName; + if ( IsX360() ) + { + Q_strncpy( szBuff, pName, sizeof( szBuff ) ); + int len = Q_strlen( szBuff )-4; + if ( len > 0 && !Q_strnicmp( szBuff+len, ".mp3", 4 ) ) + { + // convert unsupported .mp3 to .wav + Q_strcpy( szBuff+len, ".wav" ); + } + pName = szBuff; + + if ( pName[0] == CHAR_STREAM ) + { + // streaming (or not) is hardcoded to alternate criteria + // prevent the same sound from creating disparate instances + pName++; + } + } + + // see if already loaded + FileNameHandle_t fnHandle = g_pFileSystem->FindOrAddFileName( pName ); + i = s_Sounds.Find( fnHandle ); + if ( i != s_Sounds.InvalidIndex() ) + { + sfx = s_Sounds[i].pSfx; + Assert( sfx ); + if ( pfInCache ) + { + // indicate whether or not sound is currently in the cache. + *pfInCache = ( sfx->pSource && sfx->pSource->IsCached() ) ? 1 : 0; + } + return sfx; + } + else + { + SfxDictEntry entry; + entry.pSfx = ( CSfxTable * )s_SoundPool.Alloc(); + + Assert( entry.pSfx ); + + i = s_Sounds.Insert( fnHandle, entry ); + sfx = s_Sounds[i].pSfx; + + sfx->SetNamePoolIndex( i ); + sfx->pSource = NULL; + + if ( pfInCache ) + { + *pfInCache = 0; + } + } + return sfx; +} + +//----------------------------------------------------------------------------- +// S_LoadSound +// +// Check to see if wave data is in the cache. If so, return pointer to data. +// If not, allocate cache space for wave data, load wave file into temporary heap +// space, and dump/convert file data into cache. +//----------------------------------------------------------------------------- +double g_flAccumulatedSoundLoadTime = 0.0f; +CAudioSource *S_LoadSound( CSfxTable *pSfx, channel_t *ch ) +{ + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); + + VPROF("S_LoadSound"); + if ( !pSfx->pSource ) + { + if ( IsX360() ) + { + if ( SND_IsInGame() && !g_pQueuedLoader->IsMapLoading() ) + { + // sound should be present (due to reslists), but NOT allowing a load hitch during gameplay + // loading a sound during gameplay is a bad experience, causes a very expensive sync i/o to fetch the header + // and in the case of a memory wave, the actual audio data + bool bFound = false; + if ( !pSfx->m_bIsLateLoad ) + { + if ( pSfx->getname() != PSkipSoundChars( pSfx->getname() ) ) + { + // the sound might already exist as an undecorated audio source + FileNameHandle_t fnHandle = g_pFileSystem->FindOrAddFileName( pSfx->GetFileName() ); + int i = s_Sounds.Find( fnHandle ); + if ( i != s_Sounds.InvalidIndex() ) + { + CSfxTable *pOtherSfx = s_Sounds[i].pSfx; + Assert( pOtherSfx ); + CAudioSource *pOtherSource = pOtherSfx->pSource; + if ( pOtherSource && pOtherSource->IsCached() ) + { + // Can safely let the "load" continue because the headers are expected to be in the preload + // that are now persisted and the wave data cache will find an existing audio buffer match, + // so no sync i/o should occur from either. + bFound = true; + } + } + } + + if ( !bFound ) + { + // warn once + DevWarning( "S_LoadSound: Late load '%s', skipping.\n", pSfx->getname() ); + pSfx->m_bIsLateLoad = true; + } + } + + if ( !bFound ) + { + return NULL; + } + } + else if ( pSfx->m_bIsLateLoad ) + { + // outside of gameplay, let the load happen + pSfx->m_bIsLateLoad = false; + } + } + + double st = Plat_FloatTime(); + + bool bStream = false; + bool bUserVox = false; + + // sound chars can explicitly categorize usage + bStream = TestSoundChar( pSfx->getname(), CHAR_STREAM ); + if ( !bStream ) + { + bUserVox = TestSoundChar( pSfx->getname(), CHAR_USERVOX ); + } + + // override streaming + if ( IsX360() ) + { + const char *s_CriticalSounds[] = + { + "common", + "items", + "physics", + "player", + "ui", + "weapons", + }; + + // stream everything but critical sounds + bStream = true; + const char *pFileName = pSfx->GetFileName(); + for ( int i = 0; i<ARRAYSIZE( s_CriticalSounds ); i++ ) + { + size_t len = strlen( s_CriticalSounds[i] ); + if ( !Q_strnicmp( pFileName, s_CriticalSounds[i], len ) && ( pFileName[len] == '\\' || pFileName[len] == '/' ) ) + { + // never stream these, regardless of sound chars + bStream = false; + break; + } + } + } + + if ( bStream ) + { + // setup as a streaming resource + pSfx->pSource = Audio_CreateStreamedWave( pSfx ); + } + else + { + if ( bUserVox ) + { + if ( !IsX360() ) + { + pSfx->pSource = Voice_SetupAudioSource( ch->soundsource, ch->entchannel ); + } + else + { + // not supporting + Assert( 0 ); + } + } + else + { + // load all into memory directly + pSfx->pSource = Audio_CreateMemoryWave( pSfx ); + } + } + + double ed = Plat_FloatTime(); + g_flAccumulatedSoundLoadTime += ( ed - st ); + } + else + { + pSfx->pSource->CheckAudioSourceCache(); + } + + if ( !pSfx->pSource ) + { + return NULL; + } + + // first time to load? Create the mixer + if ( ch && !ch->pMixer ) + { + ch->pMixer = pSfx->pSource->CreateMixer( ch->initialStreamPosition ); + if ( !ch->pMixer ) + { + return NULL; + } + } + + return pSfx->pSource; +} + +//----------------------------------------------------------------------------- +// S_PrecacheSound +// +// Reserve space for the name of the sound in a global array. +// Load the data for the non-streaming sound. Streaming sounds +// defer loading of data until just before playback. +//----------------------------------------------------------------------------- +CSfxTable *S_PrecacheSound( const char *name ) +{ + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); + + if ( !g_AudioDevice ) + return NULL; + + if ( !g_AudioDevice->IsActive() ) + return NULL; + + CSfxTable *sfx = S_FindName( name, NULL ); + if ( sfx ) + { + // cache sound + S_LoadSound( sfx, NULL ); + } + else + { + Assert( !"S_PrecacheSound: Failed to create sfx" ); + } + + return sfx; +} + + +void S_InternalReloadSound( CSfxTable *sfx ) +{ + if ( !sfx || !sfx->pSource ) + return; + + sfx->pSource->CacheUnload(); + + delete sfx->pSource; + sfx->pSource = NULL; + + char pExt[10]; + Q_ExtractFileExtension( sfx->getname(), pExt, sizeof(pExt) ); + int nSource = !Q_stricmp( pExt, "mp3" ) ? CAudioSource::AUDIO_SOURCE_MP3 : CAudioSource::AUDIO_SOURCE_WAV; +// audiosourcecache->RebuildCacheEntry( nSource, sfx->IsPrecachedSound(), sfx ); + audiosourcecache->GetInfo( nSource, sfx->IsPrecachedSound(), sfx ); // Do a size/date check and rebuild the cache entry if necessary. +} + + +//----------------------------------------------------------------------------- +// Refresh a sound in the cache +//----------------------------------------------------------------------------- +void S_ReloadSound( const char *name ) +{ + if ( IsX360() ) + { + // not supporting + Assert( 0 ); + return; + } + + if ( !g_AudioDevice ) + return; + + if ( !g_AudioDevice->IsActive() ) + return; + + CSfxTable *sfx = S_FindName( name, NULL ); +#ifdef _DEBUG + if ( sfx ) + { + Assert( Q_stricmp( sfx->getname(), name ) == 0 ); + } +#endif + + S_InternalReloadSound( sfx ); +} + + +// See comments on CL_HandlePureServerWhitelist for details of what we're doing here. +void S_ReloadFilesInList( IFileList *pFilesToReload ) +{ + if ( !IsPC() ) + return; + + S_StopAllSounds( true ); + wavedatacache->Flush(); + audiosourcecache->ForceRecheckDiskInfo(); // Force all cached audio data to recheck size/date info next time it's accessed. + + + CUtlVector< CSfxTable * > processed; + + int iLast = s_Sounds.LastInorder(); + for ( int i = s_Sounds.FirstInorder(); i != iLast; i = s_Sounds.NextInorder( i ) ) + { + FileNameHandle_t fnHandle = s_Sounds.Key( i ); + char filename[MAX_PATH * 3]; + if ( !g_pFileSystem->String( fnHandle, filename, sizeof( filename ) ) ) + { + Assert( !"S_HandlePureServerWhitelist - can't get a filename." ); + continue; + } + + // If the file isn't cached in yet, then the filesystem hasn't touched its file, so don't bother. + CSfxTable *sfx = s_Sounds[i].pSfx; + if ( sfx && ( processed.Find( sfx ) == processed.InvalidIndex() ) ) + { + char fullFilename[MAX_PATH*2]; + if ( IsSoundChar( filename[0] ) ) + Q_snprintf( fullFilename, sizeof( fullFilename ), "sound/%s", &filename[1] ); + else + Q_snprintf( fullFilename, sizeof( fullFilename ), "sound/%s", filename ); + + + if ( !pFilesToReload->IsFileInList( fullFilename ) ) + continue; + + processed.AddToTail( sfx ); + + S_InternalReloadSound( sfx ); + } + } +} + +//----------------------------------------------------------------------------- +// Unfortunate confusing terminology. +// Here prefetching means hinting to the audio source (which may be a stream) +// to get its async data in flight. +//----------------------------------------------------------------------------- +void S_PrefetchSound( char const *name, bool bPlayOnce ) +{ + CSfxTable *sfx; + + if ( !g_AudioDevice ) + return; + + if ( !g_AudioDevice->IsActive() ) + return; + + sfx = S_FindName( name, NULL ); + if ( sfx ) + { + // cache sound + S_LoadSound( sfx, NULL ); + } + + if ( !sfx || !sfx->pSource ) + { + return; + } + + // hint the sound to start loading + sfx->pSource->Prefetch(); + + if ( bPlayOnce ) + { + sfx->pSource->SetPlayOnce( true ); + } +} + +void S_MarkUISound( CSfxTable *pSfx ) +{ + pSfx->m_bIsUISound = true; +} + +unsigned int RemainingSamples( channel_t *pChannel ) +{ + if ( !pChannel || !pChannel->sfx || !pChannel->sfx->pSource ) + return 0; + + unsigned int timeleft = pChannel->sfx->pSource->SampleCount(); + + if ( pChannel->sfx->pSource->IsLooped() ) + { + return pChannel->sfx->pSource->SampleRate(); + } + + if ( pChannel->pMixer ) + { + timeleft -= pChannel->pMixer->GetSamplePosition(); + } + + return timeleft; +} + +// chooses the voice stealing algorithm +ConVar voice_steal("voice_steal", "2"); + +/* +================= +SND_StealDynamicChannel +Select a channel from the dynamic channel allocation area. For the given entity, +override any other sound playing on the same channel (see code comments below for +exceptions). +================= +*/ +channel_t *SND_StealDynamicChannel(SoundSource soundsource, int entchannel, const Vector &origin, CSfxTable *sfx, float flDelay, bool bDoNotOverwriteExisting) +{ + int canSteal[MAX_DYNAMIC_CHANNELS]; + int canStealCount = 0; + + int sameSoundCount = 0; + unsigned int sameSoundRemaining = 0xFFFFFFFF; + int sameSoundIndex = -1; + int sameVol = 0xFFFF; + int availableChannel = -1; + bool bDelaySame = false; + + int nExactMatch[MAX_DYNAMIC_CHANNELS]; + int nExactCount = 0; + // first pass to replace sounds on same ent/channel, and search for free or stealable channels otherwise + for ( int ch_idx = 0; ch_idx < MAX_DYNAMIC_CHANNELS ; ch_idx++) + { + channel_t *ch = &channels[ch_idx]; + + if ( ch->activeIndex ) + { + // channel CHAN_AUTO never overrides sounds on same channel + if ( entchannel != CHAN_AUTO ) + { + int checkChannel = entchannel; + if ( checkChannel == -1 ) + { + if ( ch->entchannel != CHAN_STREAM && ch->entchannel != CHAN_VOICE && ch->entchannel != CHAN_VOICE2 ) + { + checkChannel = ch->entchannel; + } + } + if ( ch->soundsource == soundsource && (soundsource != -1) && ch->entchannel == checkChannel ) + { + // we found an exact match for this entity and this channel, but the sound we want to play is considered + // low priority so instead of stomping this entry pretend we couldn't find a free slot to play and let + // the existing sound keep going + if ( bDoNotOverwriteExisting ) + return NULL; + + if ( ch->flags.delayed_start ) + { + nExactMatch[nExactCount] = ch_idx; + nExactCount++; + continue; + } + return ch; // always override sound from same entity + } + } + + // Never steal the channel of a streaming sound that is currently playing or + // voice over IP data that is playing or any sound on CHAN_VOICE( acting ) + if ( ch->entchannel == CHAN_STREAM || ch->entchannel == CHAN_VOICE || ch->entchannel == CHAN_VOICE2 ) + continue; + + // don't let monster sounds override player sounds + if ( g_pSoundServices->IsPlayer( ch->soundsource ) && !g_pSoundServices->IsPlayer(soundsource) ) + continue; + + if ( ch->sfx == sfx ) + { + bDelaySame = ch->flags.delayed_start ? true : bDelaySame; + sameSoundCount++; + int maxVolume = ChannelGetMaxVol( ch ); + unsigned int remaining = RemainingSamples(ch); + if ( maxVolume < sameVol || (maxVolume == sameVol && remaining < sameSoundRemaining) ) + { + sameSoundIndex = ch_idx; + sameVol = maxVolume; + sameSoundRemaining = remaining; + } + } + canSteal[canStealCount++] = ch_idx; + } + else + { + if ( availableChannel < 0 ) + { + availableChannel = ch_idx; + } + } + } + + + // coalesce the timeline for this channel + if ( nExactCount > 0 ) + { + uint nFreeSampleTime = g_paintedtime + (flDelay * SOUND_DMA_SPEED); + channel_t *pReturn = &channels[nExactMatch[0]]; + uint nMinRemaining = RemainingSamples( pReturn ); + if ( pReturn->nFreeChannelAtSampleTime == 0 || pReturn->nFreeChannelAtSampleTime > nFreeSampleTime ) + { + pReturn->nFreeChannelAtSampleTime = nFreeSampleTime; + } + for ( int i = 1; i < nExactCount; i++ ) + { + channel_t *pChannel = &channels[nExactMatch[i]]; + if ( pChannel->nFreeChannelAtSampleTime == 0 || pChannel->nFreeChannelAtSampleTime > nFreeSampleTime ) + { + pChannel->nFreeChannelAtSampleTime = nFreeSampleTime; + } + uint nRemain = RemainingSamples( pChannel ); + if ( nRemain < nMinRemaining ) + { + pReturn = pChannel; + nMinRemaining = nRemain; + } + } + // if there's only one, mark it to be freed but don't reuse it. + // otherwise mark all others to be freed and use the closest one to being done + if ( nExactCount > 1 ) + { + return pReturn; + } + } + + // Limit the number of times a given sfx/wave can play simultaneously + if ( voice_steal.GetInt() > 1 && sameSoundIndex >= 0 ) + { + // if sounds of this type are normally delayed, then add an extra slot for stealing + // NOTE: In HL2 these are usually NPC gunshot sounds - and stealing too soon will cut + // them off early. This is a safe heuristic to avoid that problem. There's probably a better + // long-term solution involving only counting channels that are actually going to play (delay included) + // at the same time as this one. + int maxSameSounds = bDelaySame ? 5 : 4; + float distSqr = 0.0f; + if ( sfx->pSource ) + { + distSqr = origin.DistToSqr(listener_origin); + if ( sfx->pSource->IsLooped() ) + { + maxSameSounds = 3; + } + } + + // don't play more than N copies of the same sound, steal the quietest & closest one otherwise + if ( sameSoundCount >= maxSameSounds ) + { + channel_t *ch = &channels[sameSoundIndex]; + // you're already playing a closer version of this sound, don't steal + if ( distSqr > 0.0f && ch->origin.DistToSqr(listener_origin) < distSqr && entchannel != CHAN_WEAPON ) + return NULL; + + //Msg("Sound playing %d copies, stole %s (%d)\n", sameSoundCount, ch->sfx->getname(), sameVol ); + return ch; + } + } + + // if there's a free channel, just take that one - don't steal + if ( availableChannel >= 0 ) + return &channels[availableChannel]; + + // Still haven't found a suitable channel, so choose the one with the least amount of time left to play + float life_left = FLT_MAX; + int first_to_die = -1; + bool bAllowVoiceSteal = voice_steal.GetBool(); + + for ( int i = 0; i < canStealCount; i++ ) + { + int ch_idx = canSteal[i]; + channel_t *ch = &channels[ch_idx]; + float timeleft = 0; + if ( bAllowVoiceSteal ) + { + int maxVolume = ChannelGetMaxVol( ch ); + if ( maxVolume < 5 ) + { + //Msg("Sound quiet, stole %s for %s\n", ch->sfx->getname(), sfx->getname() ); + return ch; + } + + if ( ch->sfx && ch->sfx->pSource ) + { + unsigned int sampleCount = RemainingSamples( ch ); + timeleft = (float)sampleCount / (float)ch->sfx->pSource->SampleRate(); + } + } + else + { + // UNDONE: Kill this when voice_steal 0,1,2 has been tested + // UNDONE: This is the old buggy code that we're trying to replace + if ( ch->sfx ) + { + // basically steals the first one you come to + timeleft = 1; //ch->end - paintedtime + } + } + + if ( timeleft < life_left ) + { + life_left = timeleft; + first_to_die = ch_idx; + } + } + if ( first_to_die >= 0 ) + { + //Msg("Stole %s, timeleft %d\n", channels[first_to_die].sfx->getname(), life_left ); + return &channels[first_to_die]; + } + + return NULL; +} + +channel_t *SND_PickDynamicChannel(SoundSource soundsource, int entchannel, const Vector &origin, CSfxTable *sfx, float flDelay, bool bDoNotOverwriteExisting) +{ + channel_t *pChannel = SND_StealDynamicChannel( soundsource, entchannel, origin, sfx, flDelay, bDoNotOverwriteExisting ); + if ( !pChannel ) + return NULL; + + if (pChannel->sfx) + { + // Don't restart looping sounds for the same entity + CAudioSource *pSource = pChannel->sfx->pSource; + if ( pSource ) + { + if ( pSource->IsLooped() ) + { + if ( pChannel->soundsource == soundsource && pChannel->entchannel == entchannel && pChannel->sfx == sfx ) + { + // same looping sound, same ent, same channel, don't restart the sound + return NULL; + } + } + } + // be sure and release previous channel + // if sentence. + // ("Stealing channel from %s\n", channels[first_to_die].sfx->getname() ); + S_FreeChannel(pChannel); + } + + return pChannel; +} + + + +/* +===================== +SND_PickStaticChannel +===================== +Pick an empty channel from the static sound area, or allocate a new +channel. Only fails if we're at max_channels (128!!!) or if +we're trying to allocate a channel for a stream sound that is +already playing. + +*/ +channel_t *SND_PickStaticChannel(int soundsource, CSfxTable *pSfx) +{ + int i; + channel_t *ch = NULL; + + // Check for replacement sound, or find the best one to replace + for (i = MAX_DYNAMIC_CHANNELS; i<total_channels; i++) + if (channels[i].sfx == NULL) + break; + + if (i < total_channels) + { + // reuse an empty static sound channel + ch = &channels[i]; + } + else + { + // no empty slots, alloc a new static sound channel + if (total_channels == MAX_CHANNELS) + { + DevMsg ("total_channels == MAX_CHANNELS\n"); + return NULL; + } + + // get a channel for the static sound + ch = &channels[total_channels]; + total_channels++; + } + + return ch; +} + + +void S_SpatializeChannel( int pVolume[CCHANVOLUMES/2], int master_vol, const Vector *psourceDir, float gain, float mono ) +{ + float lscale, rscale, scale; + vec_t dotRight; + Vector sourceDir = *psourceDir; + + dotRight = DotProduct(listener_right, sourceDir); + + // clear volumes + for (int i = 0; i < CCHANVOLUMES/2; i++) + pVolume[i] = 0; + + if (mono > 0.0) + { + // sound has radius, within which spatialization becomes mono: + + // mono is 0.0 -> 1.0, from radius 100% to radius 50% + + // at radius * 0.5, dotRight is 0 (ie: sound centered left/right) + // at radius * 1.0, dotRight == dotRight + + dotRight *= (1.0 - mono); + } + + rscale = 1.0 + dotRight; + lscale = 1.0 - dotRight; + + // add in distance effect + scale = gain * rscale / 2; + pVolume[IFRONT_RIGHT] = (int) (master_vol * scale); + + scale = gain * lscale / 2; + pVolume[IFRONT_LEFT] = (int) (master_vol * scale); + + pVolume[IFRONT_RIGHT] = clamp( pVolume[IFRONT_RIGHT], 0, 255 ); + pVolume[IFRONT_LEFT] = clamp( pVolume[IFRONT_LEFT], 0, 255 ); + +} + +bool S_IsMusic( channel_t *pChannel ) +{ + if ( !pChannel->flags.bdry ) + return false; + + CSfxTable *sfx = pChannel->sfx; + if ( !sfx ) + return false; + + CAudioSource *source = sfx->pSource; + if ( !source ) + return false; + + // Don't save restore looping sounds as you can end up with an entity restarting them again and have + // them accumulate, etc. + if ( source->IsLooped() ) + return false; + + CAudioMixer *pMixer = pChannel->pMixer; + if ( !pMixer ) + return false; + + for ( int i = 0; i < 8; i++ ) + { + if ( pChannel->mixgroups[i] != -1 ) + { + char *pGroupName = MXR_GetGroupnameFromId( pChannel->mixgroups[i] ); + if ( !Q_strcmp( pGroupName, "Music" ) ) + { + return true; + } + } + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: For save/restore of currently playing music +// Input : list - +//----------------------------------------------------------------------------- +void S_GetCurrentlyPlayingMusic( CUtlVector< musicsave_t >& musiclist ) +{ + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = &channels[list.GetChannelIndex(i)]; + if ( !S_IsMusic( pChannel ) ) + continue; + + musicsave_t song; + Q_strncpy( song.songname, pChannel->sfx->getname(), sizeof( song.songname ) ); + song.sampleposition = pChannel->pMixer->GetPositionForSave(); + song.master_volume = pChannel->master_vol; + + musiclist.AddToTail( song ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *song - +//----------------------------------------------------------------------------- +void S_RestartSong( const musicsave_t *song ) +{ + Assert( song ); + + // Start the song + CSfxTable *pSound = S_PrecacheSound( song->songname ); + if ( pSound ) + { + StartSoundParams_t params; + params.staticsound = true; + params.soundsource = SOUND_FROM_WORLD; + params.entchannel = CHAN_STATIC; + params.pSfx = pSound; + params.origin = vec3_origin; + params.fvol = ( (float)song->master_volume / 255.0f ); + params.soundlevel = SNDLVL_NONE; + params.flags = SND_NOFLAGS; + params.pitch = PITCH_NORM; + params.initialStreamPosition = song->sampleposition; + + S_StartSound( params ); + + if ( IsPC() ) + { + // Now find the channel this went on and skip ahead in the mixer + for (int i = 0; i < total_channels; i++) + { + channel_t *ch = &channels[i]; + + if ( !ch->pMixer || + !ch->pMixer->GetSource() ) + { + continue; + } + + if ( ch->pMixer->GetSource() != pSound->pSource ) + { + continue; + } + + ch->pMixer->SetPositionFromSaved( song->sampleposition ); + break; + } + } + } +} + +soundlevel_t SND_GetSndlvl ( channel_t *pchannel ); + +// calculate ammount of sound to be mixed to dsp, based on distance from listener + + +ConVar dsp_dist_min("dsp_dist_min", "0.0", FCVAR_DEMO|FCVAR_CHEAT); // range at which sounds are mixed at dsp_mix_min +ConVar dsp_dist_max("dsp_dist_max", "1440.0", FCVAR_DEMO|FCVAR_CHEAT); // range at which sounds are mixed at dsp_mix_max + +ConVar dsp_mix_min("dsp_mix_min", "0.2", FCVAR_DEMO ); // dsp mix at dsp_dist_min distance "near" +ConVar dsp_mix_max("dsp_mix_max", "0.8", FCVAR_DEMO ); // dsp mix at dsp_dist_max distance "far" +ConVar dsp_db_min("dsp_db_min", "80", FCVAR_DEMO ); // sounds with sndlvl below this get dsp_db_mixdrop % less dsp mix +ConVar dsp_db_mixdrop("dsp_db_mixdrop", "0.5", FCVAR_DEMO ); // sounds with sndlvl below dsp_db_min get dsp_db_mixdrop % less mix + +float DSP_ROOM_MIX = 1.0; // mix volume of dsp_room sounds when added back to 'dry' sounds +float DSP_NOROOM_MIX = 1.0; // mix volume of facing + facing away sounds. added to dsp_room_mix sounds + +extern ConVar dsp_off; + +// returns 0-1.0 dsp mix value. If sound source is at a range >= DSP_DIST_MAX, return a mix value of +// DSP_MIX_MAX. This mix value is used later to determine wet/dry mix ratio of sounds. + +// This ramp changes with db level of sound source, and is set in the dsp room presets by room size +// empirical data: 0.78 is nominal mix for sound 100% at far end of room, 0.24 is mix for sound 25% into room + +float SND_GetDspMix( channel_t *pchannel, int idist) +{ + float mix; + float dist = (float)idist; + float dist_min = dsp_dist_min.GetFloat(); + float dist_max = dsp_dist_max.GetFloat(); + float mix_min; + float mix_max; + + // only set dsp mix_min & mix_max when sound is first started + + if ( pchannel->dsp_mix_min < 0 && pchannel->dsp_mix_max < 0 ) + { + mix_min = dsp_mix_min.GetFloat(); // set via dsp_room preset + mix_max = dsp_mix_max.GetFloat(); // set via dsp_room preset + + // set mix_min & mix_max based on db level of sound: + // sounds below dsp_db_min decrease dsp_mix_min & dsp_mix_max by N% + // ie: quiet sounds get less dsp mix than loud sounds + + soundlevel_t sndlvl = SND_GetSndlvl( pchannel ); + soundlevel_t sndlvl_min = (soundlevel_t)(dsp_db_min.GetInt()); + + if (sndlvl <= sndlvl_min) + { + mix_min *= dsp_db_mixdrop.GetFloat(); + mix_max *= dsp_db_mixdrop.GetFloat(); + } + + pchannel->dsp_mix_min = mix_min; + pchannel->dsp_mix_max = mix_max; + } + else + { + mix_min = pchannel->dsp_mix_min; + mix_max = pchannel->dsp_mix_max; + } + + // dspmix is 0 (100% mix to facing buffer) if dsp_off + + if ( dsp_off.GetInt() ) + return 0.0; + + // doppler wavs are mixed dry + + if ( pchannel->wavtype == CHAR_DOPPLER ) + return 0.0; + + // linear ramp - get dry mix % + + // dist: 0->(max - min) + + dist = clamp( dist, dist_min, dist_max ) - dist_min; + + // dist: 0->1.0 + + dist = dist / (dist_max - dist_min); + + // mix: min->max + + mix = ((mix_max - mix_min) * dist) + mix_min; + + return mix; +} + +// calculate crossfade between wav left (close sound) and wav right (far sound) based on +// distance fron listener + +#define DVAR_DIST_MIN (20.0 * 12.0) // play full 'near' sound at 20' or less +#define DVAR_DIST_MAX (110.0 * 12.0) // play full 'far' sound at 110' or more +#define DVAR_MIX_MIN 0.0 +#define DVAR_MIX_MAX 1.0 + +// calculate mixing parameter for CHAR_DISTVAR wavs +// returns 0 - 1.0, 1.0 is 100% far sound (wav right) + +float SND_GetDistanceMix( channel_t *pchannel, int idist) +{ + float mix; + float dist = (float)idist; + + // doppler wavs are 100% near - their spatialization is calculated later. + + if ( pchannel->wavtype == CHAR_DOPPLER ) + return 0.0; + + // linear ramp - get dry mix % + + // dist 0->(max - min) + + dist = clamp( dist, (float) DVAR_DIST_MIN, (float) DVAR_DIST_MAX ) - (float) DVAR_DIST_MIN; + + // dist 0->1.0 + + dist = dist / (DVAR_DIST_MAX - DVAR_DIST_MIN); + + // mix min->max + + mix = ((DVAR_MIX_MAX - DVAR_MIX_MIN) * dist) + DVAR_MIX_MIN; + + return mix; +} + +// given facing direction of source, and channel, +// return -1.0 - 1.0, where -1.0 is source facing away from listener +// and 1.0 is source facing listener + + +float SND_GetFacingDirection( channel_t *pChannel, const QAngle &source_angles ) +{ + Vector SF; // sound source forward direction unit vector + Vector SL; // sound -> listener unit vector + float dotSFSL; + + // no facing direction unless wavtyp CHAR_DIRECTIONAL + + if ( pChannel->wavtype != CHAR_DIRECTIONAL ) + return 1.0; + + VectorSubtract(listener_origin, pChannel->origin, SL); + VectorNormalize(SL); + + // compute forward vector for sound entity + + AngleVectors( source_angles, &SF, NULL, NULL ); + + // dot source forward unit vector with source to listener unit vector to get -1.0 - 1.0 facing. + // ie: projection of SF onto SL + + dotSFSL = DotProduct( SF, SL ); + + return dotSFSL; +} + +// calculate point of closest approach - caller must ensure that the +// forward facing vector of the entity playing this sound points in exactly the direction of +// travel of the sound. ie: for bullets or tracers, forward vector must point in traceline direction. +// return true if sound is to be played, false if sound cannot be heard (shot away from player) + +bool SND_GetClosestPoint( channel_t *pChannel, QAngle &source_angles, Vector &vnearpoint ) +{ + // S - sound source origin + // L - listener origin + + Vector SF; // sound source forward direction unit vector + Vector SL; // sound -> listener vector + Vector SD; // sound->closest point vector + vec_t dSLSF; // magnitude of project of SL onto SF + + // P = SF (SF . SL) + S + + // only perform this calculation for doppler wavs + + if ( pChannel->wavtype != CHAR_DOPPLER ) + return false; + + // get vector 'SL' from sound source to listener + + VectorSubtract(listener_origin, pChannel->origin, SL); + + // compute sound->forward vector 'SF' for sound entity + + AngleVectors( source_angles, &SF ); + VectorNormalize( SF ); + + dSLSF = DotProduct( SL, SF ); + + + if ( dSLSF <= 0 && !toolframework->IsToolRecording() ) + { + // source is pointing away from listener, don't play anything + // unless we're recording in the tool, since we may play back from in front of the source + return false; + } + + // project dSLSF along forward unit vector from sound source + + VectorMultiply( SF, dSLSF, SD ); + + // output vector - add SD to sound source origin + + VectorAdd( SD, pChannel->origin, vnearpoint ); + + return true; +} + + +// given point of nearest approach and sound source facing angles, +// return vector pointing into quadrant in which to play +// doppler left wav (incomming) and doppler right wav (outgoing). + +// doppler left is point in space to play left doppler wav +// doppler right is point in space to play right doppler wav + +// Also modifies channel pitch based on distance to nearest approach point + +#define DOPPLER_DIST_LEFT_TO_RIGHT (4*12) // separate left/right sounds by 4' + +#define DOPPLER_DIST_MAX (20*12) // max distance - causes min pitch +#define DOPPLER_DIST_MIN (1*12) // min distance - causes max pitch +#define DOPPLER_PITCH_MAX 1.5 // max pitch change due to distance +#define DOPPLER_PITCH_MIN 0.25 // min pitch change due to distance + +#define DOPPLER_RANGE_MAX (10*12) // don't play doppler wav unless within this range + // UNDONE: should be set by caller! + +void SND_GetDopplerPoints( channel_t *pChannel, QAngle &source_angles, Vector &vnearpoint, Vector &source_doppler_left, Vector &source_doppler_right) +{ + Vector SF; // direction sound source is facing (forward) + Vector LN; // vector from listener to closest approach point + Vector DL; + Vector DR; + + // nearpoint is closest point of approach, when playing CHAR_DOPPLER sounds + + // SF is normalized vector in direction sound source is facing + + AngleVectors( source_angles, &SF ); + VectorNormalize( SF ); + + // source_doppler_left - location in space to play doppler left wav (incomming) + // source_doppler_right - location in space to play doppler right wav (outgoing) + + VectorMultiply( SF, -1*DOPPLER_DIST_LEFT_TO_RIGHT, DL ); + VectorMultiply( SF, DOPPLER_DIST_LEFT_TO_RIGHT, DR ); + + VectorAdd( vnearpoint, DL, source_doppler_left ); + VectorAdd( vnearpoint, DR, source_doppler_right ); + + // set pitch of channel based on nearest distance to listener + + // LN is vector from listener to closest approach point + + VectorSubtract(vnearpoint, listener_origin, LN); + + float pitch; + float dist = VectorLength( LN ); + + // dist varies 0->1 + + dist = clamp(dist, (float)DOPPLER_DIST_MIN, (float)DOPPLER_DIST_MAX); + dist = (dist - DOPPLER_DIST_MIN) / (DOPPLER_DIST_MAX - DOPPLER_DIST_MIN); + + // pitch varies from max to min + + pitch = DOPPLER_PITCH_MAX - dist * (DOPPLER_PITCH_MAX - DOPPLER_PITCH_MIN); + + pChannel->basePitch = (int)(pitch * 100.0); +} + +// console variables used to construct gain curve - don't change these! + +extern ConVar snd_foliage_db_loss; +extern ConVar snd_gain; +extern ConVar snd_refdb; +extern ConVar snd_refdist; +extern ConVar snd_gain_max; +extern ConVar snd_gain_min; + +ConVar snd_showstart( "snd_showstart", "0", FCVAR_CHEAT ); // showstart always skips info on player footsteps! + // 1 - show sound name, channel, volume, time + // 2 - show dspmix, distmix, dspface, l/r/f/r vols + // 3 - show sound origin coords + // 4 - show gain of dsp_room + // 5 - show dB loss due to obscured sound + // 6 - reserved + // 7 - show 2 and total gain & dist in ft. to sound source + +#define SND_DB_MAX 140.0 // max db of any sound source +#define SND_DB_MED 90.0 // db at which compression curve changes +#define SND_DB_MIN 60.0 // min db of any sound source + +#define SND_GAIN_PLAYER_WEAPON_DB 2.0 // increase player weapon gain by N dB + +// dB = 20 log (amplitude/32768) 0 to -90.3dB +// amplitude = 32768 * 10 ^ (dB/20) 0 to +/- 32768 +// gain = amplitude/32768 0 to 1.0 + +float Gain_To_dB ( float gain ) +{ + float dB = 20 * log ( gain ); + return dB; +} + +float dB_To_Gain ( float dB ) +{ + float gain = powf (10, dB / 20.0); + return gain; +} + +float Gain_To_Amplitude ( float gain ) +{ + return gain * 32768; +} + +float Amplitude_To_Gain ( float amplitude ) +{ + return amplitude / 32768; +} + +soundlevel_t SND_GetSndlvl ( channel_t *pchannel ) +{ + return DIST_MULT_TO_SNDLVL( pchannel->dist_mult ); +} + + +// The complete gain calculation, with SNDLVL given in dB is: +// +// GAIN = 1/dist * snd_refdist * 10 ^ ( ( SNDLVL - snd_refdb - (dist * snd_foliage_db_loss / 1200)) / 20 ) +// +// for gain > SND_GAIN_THRESH, start curve smoothing with +// +// GAIN = 1 - 1 / (Y * GAIN ^ SND_GAIN_POWER) +// +// where Y = -1 / ( (SND_GAIN_THRESH ^ SND_GAIN_POWER) * (SND_GAIN_THRESH - 1) ) +// + +float SND_GetGainFromMult( float gain, float dist_mult, vec_t dist ); + +// gain curve construction + +float SND_GetGain( channel_t *ch, bool fplayersound, bool fmusicsound, bool flooping, vec_t dist, bool bAttenuated ) +{ + VPROF_("SND_GetGain",2,VPROF_BUDGETGROUP_OTHER_SOUND,false,BUDGETFLAG_OTHER); + if ( ch->flags.m_bCompatibilityAttenuation ) + { + // Convert to the original attenuation value. + soundlevel_t soundlevel = DIST_MULT_TO_SNDLVL( ch->dist_mult ); + float flAttenuation = SNDLVL_TO_ATTN( soundlevel ); + + // Now get the goldsrc dist_mult and use the same calculation it uses in SND_Spatialize. + // Straight outta Goldsrc!!! + vec_t nominal_clip_dist = 1000.0; + float flGoldsrcDistMult = flAttenuation / nominal_clip_dist; + dist *= flGoldsrcDistMult; + float flReturnValue = 1.0f - dist; + flReturnValue = clamp( flReturnValue, 0.f, 1.f ); + return flReturnValue; + } + else + { + float gain = snd_gain.GetFloat(); + + if ( fmusicsound ) + { + gain = gain * snd_musicvolume.GetFloat(); + gain = gain * g_DashboardMusicMixValue; + } + + if ( ch->dist_mult ) + { + gain = SND_GetGainFromMult( gain, ch->dist_mult, dist ); + } + + if ( fplayersound ) + { + + // player weapon sounds get extra gain - this compensates + // for npc distance effect weapons which mix louder as L+R into L,R + // Hack. + + if ( ch->entchannel == CHAN_WEAPON ) + gain = gain * dB_To_Gain( SND_GAIN_PLAYER_WEAPON_DB ); + } + + // modify gain if sound source not visible to player + + gain = gain * SND_GetGainObscured( ch, fplayersound, flooping, bAttenuated ); + + if (snd_showstart.GetInt() == 6) + { + DevMsg( "(gain %1.3f : dist ft %1.1f) ", gain, (float)dist/12.0 ); + snd_showstart.SetValue(5); // display once + } + + return gain; + } +} + +// always ramp channel gain changes over time +// returns ramped gain, given new target gain + +#define SND_GAIN_FADE_TIME 0.25 // xfade seconds between obscuring gain changes + +float SND_FadeToNewGain( channel_t *ch, float gain_new ) +{ + + if ( gain_new == -1.0 ) + { + // if -1 passed in, just keep fading to existing target + + gain_new = ch->ob_gain_target; + } + + // if first time updating, store new gain into gain & target, return + // if gain_new is close to existing gain, store new gain into gain & target, return + + if ( ch->flags.bfirstpass || (fabs (gain_new - ch->ob_gain) < 0.01)) + { + ch->ob_gain = gain_new; + ch->ob_gain_target = gain_new; + ch->ob_gain_inc = 0.0; + return gain_new; + } + + // set up new increment to new target + + float frametime = g_pSoundServices->GetHostFrametime(); + float speed; + speed = ( frametime / SND_GAIN_FADE_TIME ) * (gain_new - ch->ob_gain); + + ch->ob_gain_inc = fabs(speed); + + // ch->ob_gain_inc = fabs(gain_new - ch->ob_gain) / 10.0; + + ch->ob_gain_target = gain_new; + + // if not hit target, keep approaching + + if ( fabs( ch->ob_gain - ch->ob_gain_target ) > 0.01 ) + { + ch->ob_gain = Approach( ch->ob_gain_target, ch->ob_gain, ch->ob_gain_inc ); + } + else + { + // close enough, set gain = target + ch->ob_gain = ch->ob_gain_target; + } + + return ch->ob_gain; +} + +#define SND_TRACE_UPDATE_MAX 2 // max of N channels may be checked for obscured source per frame + +static int g_snd_trace_count = 0; // total tracelines for gain obscuring made this frame + +// All new sounds must traceline once, +// but cap the max number of tracelines performed per frame +// for longer or looping sounds to SND_TRACE_UPDATE_MAX. + +bool SND_ChannelOkToTrace( channel_t *ch ) +{ + // always trace first time sound is spatialized (doesn't update counter) + + if ( ch->flags.bfirstpass ) + { + ch->flags.bTraced = true; + return true; + } + + // if already traced max channels this frame, return + + if ( g_snd_trace_count >= SND_TRACE_UPDATE_MAX ) + return false; + + // ok to trace if this sound hasn't yet been traced in this round + + if ( ch->flags.bTraced ) + return false; + + // set flag - don't traceline this sound again until all others have + // been traced + + ch->flags.bTraced = true; + + g_snd_trace_count++; // total traces this frame + + return true; +} + +// determine if we need to reset all flags for traceline limiting - +// this happens if we hit a frame whein no tracelines occur ie: all currently +// playing sounds are blocked. + +void SND_ChannelTraceReset( void ) +{ + if ( g_snd_trace_count ) + return; + + // if no tracelines performed this frame, then reset all + // trace flags + + for (int i = 0; i < total_channels; i++) + channels[i].flags.bTraced = false; +} + +bool SND_IsLongWave( channel_t *pChannel ) +{ + CAudioSource *pSource = pChannel->sfx ? pChannel->sfx->pSource : NULL; + if ( pSource ) + { + if ( pSource->IsStreaming() ) + return true; + + // UNDONE: Do this on long wave files too? +#if 0 + float length = (float)pSource->SampleCount() / (float)pSource->SampleRate(); + if ( length > 0.75f ) + return true; +#endif + } + + return false; +} + + +ConVar snd_obscured_gain_db( "snd_obscured_gain_dB", "-2.70", FCVAR_CHEAT ); // dB loss due to obscured sound source + +// drop gain on channel if sound emitter obscured by +// world, unbroken windows, closed doors, large solid entities etc. + +float SND_GetGainObscured( channel_t *ch, bool fplayersound, bool flooping, bool bAttenuated ) +{ + float gain = 1.0; + int count = 1; + float snd_gain_db; // dB loss due to obscured sound source + + // Unattenuated sounds don't get obscured. + if ( !bAttenuated ) + return 1.0f; + + if ( fplayersound ) + return gain; + + // During signon just apply regular state machine since world hasn't been + // created or settled yet... + + if ( !SND_IsInGame() ) + { + if ( !toolframework->InToolMode() ) + { + gain = SND_FadeToNewGain( ch, -1.0 ); + } + + return gain; + } + + // don't do gain obscuring more than once on short one-shot sounds + + if ( !ch->flags.bfirstpass && !ch->flags.isSentence && !flooping && !SND_IsLongWave(ch) ) + { + gain = SND_FadeToNewGain( ch, -1.0 ); + return gain; + } + + snd_gain_db = snd_obscured_gain_db.GetFloat(); + + // if long or looping sound, process N channels per frame - set 'processed' flag, clear by + // cycling through all channels - this maintains a cap on traces per frame + + if ( !SND_ChannelOkToTrace( ch ) ) + { + // just keep updating fade to existing target gain - no new trace checking + + gain = SND_FadeToNewGain( ch, -1.0 ); + return gain; + } + // set up traceline from player eyes to sound emitting entity origin + + Vector endpoint = ch->origin; + + trace_t tr; + CTraceFilterWorldOnly filter; // UNDONE: also test for static props? + Ray_t ray; + ray.Init( MainViewOrigin(), endpoint ); + g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); + + if (tr.DidHit() && tr.fraction < 0.99) + { + // can't see center of sound source: + // build extents based on dB sndlvl of source, + // test to see how many extents are visible, + // drop gain by snd_gain_db per extent hidden + + Vector endpoints[4]; + soundlevel_t sndlvl = DIST_MULT_TO_SNDLVL( ch->dist_mult ); + float radius; + Vector vsrc_forward; + Vector vsrc_right; + Vector vsrc_up; + Vector vecl; + Vector vecr; + Vector vecl2; + Vector vecr2; + int i; + + // get radius + + if ( ch->radius > 0 ) + radius = ch->radius; + else + radius = dB_To_Radius( sndlvl); // approximate radius from soundlevel + + // set up extent endpoints - on upward or downward diagonals, facing player + + for (i = 0; i < 4; i++) + endpoints[i] = endpoint; + + // vsrc_forward is normalized vector from sound source to listener + + VectorSubtract( listener_origin, endpoint, vsrc_forward ); + VectorNormalize( vsrc_forward ); + VectorVectors( vsrc_forward, vsrc_right, vsrc_up ); + + VectorAdd( vsrc_up, vsrc_right, vecl ); + + // if src above listener, force 'up' vector to point down - create diagonals up & down + + if ( endpoint.z > listener_origin.z + (10 * 12) ) + vsrc_up.z = -vsrc_up.z; + + VectorSubtract( vsrc_up, vsrc_right, vecr ); + VectorNormalize( vecl ); + VectorNormalize( vecr ); + + // get diagonal vectors from sound source + + vecl2 = radius * vecl; + vecr2 = radius * vecr; + vecl = (radius / 2.0) * vecl; + vecr = (radius / 2.0) * vecr; + + // endpoints from diagonal vectors + + endpoints[0] += vecl; + endpoints[1] += vecr; + endpoints[2] += vecl2; + endpoints[3] += vecr2; + + // drop gain for each point on radius diagonal that is obscured + + for (count = 0, i = 0; i < 4; i++) + { + // UNDONE: some endpoints are in walls - in this case, trace from the wall hit location + + ray.Init( MainViewOrigin(), endpoints[i] ); + g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); + + if (tr.DidHit() && tr.fraction < 0.99 && !tr.startsolid ) + { + count++; // skip first obscured point: at least 2 points + center should be obscured to hear db loss + if (count > 1) + gain = gain * dB_To_Gain( snd_gain_db ); + } + } + } + + + if ( flooping && snd_showstart.GetInt() == 7) + { + static float g_drop_prev = 0; + float drop = (count-1) * snd_gain_db; + + if (drop != g_drop_prev) + { + DevMsg( "dB drop: %1.4f \n", drop); + g_drop_prev = drop; + } + } + + // crossfade to new gain + + gain = SND_FadeToNewGain( ch, gain ); + + return gain; +} + +// convert sound db level to approximate sound source radius, +// used only for determining how much of sound is obscured by world + +#define SND_RADIUS_MAX (20.0 * 12.0) // max sound source radius +#define SND_RADIUS_MIN (2.0 * 12.0) // min sound source radius + +inline float dB_To_Radius ( float db ) +{ + float radius = SND_RADIUS_MIN + (SND_RADIUS_MAX - SND_RADIUS_MIN) * (db - SND_DB_MIN) / (SND_DB_MAX - SND_DB_MIN); + + return radius; +} + +struct snd_spatial_t +{ + int chan; // 0..4 cycles through up to 5 channels + int cycle; // 0..2 cycles through 3 vectors per channel + int dist[5][3]; // stores last 3 channel distance values [channel][cycle] + + float value_prev[5]; // previous value per channel + + double last_change; +}; + +bool g_ssp_init = false; +snd_spatial_t g_ssp; + +// return 0..1 percent difference between a & b + +float PercentDifference( float a, float b ) +{ + float vp; + + if (!(int)a && !(int)b) + return 0.0; + + if (!(int)a || !(int)b) + return 1.0; + + if (a > b) + vp = b / a; + else + vp = a / b; + + return (1.0 - vp); +} + +// NOTE: Do not change SND_WALL_TRACE_LEN without also changing PRC_MDY6 delay value in snd_dsp.cpp! + +#define SND_WALL_TRACE_LEN (100.0*12.0) // trace max of 100' = max of 100 milliseconds of linear delay +#define SND_SPATIAL_WAIT (0.25) // seconds to wait between traces + +// change mod delay value on chan 0..3 to v (inches) + +void DSP_SetSpatialDelay( int chan, float v ) +{ + // remap delay value 0..1200 to 1.0 to -1.0 for modulation + + float value = ( v / SND_WALL_TRACE_LEN) - 1.0; // -1.0...0 + value = value * 2.0; // -2.0...0 + value += 1.0; // -1.0...1.0 (0...1200) + value *= -1.0; // 1.0...-1.0 (0...1200) + + // assume first processor in dsp_spatial is the modulating delay unit for DSP_ChangePresetValue + + int iproc = 0; + + DSP_ChangePresetValue( idsp_spatial, chan, iproc, value ); +/* + + if (chan & 0x01) + DevMsg("RDly: %3.0f \n", v/12 ); + else + DevMsg("LDly: %3.0f \n", v/12 ); +*/ +} + +// use non-feedback delay to stereoize (or make quad, or quad + center) the mono dsp_room fx, +// This simulates the average sum of delays caused by reflections +// from the left and right walls relative to the player. The average delay +// difference between left & right wall is (l + r)/2. This becomes the average +// delay difference between left & right ear. +// call at most once per frame to update player->wall spatial delays + +void SND_SetSpatialDelays() +{ + VPROF("SoundSpatialDelays"); + float dist, v, vp; + Vector v_dir, v_dir2; + int chan_max = (g_AudioDevice->IsSurround() ? 4 : 2) + (g_AudioDevice->IsSurroundCenter() ? 1 : 0); // 2, 4, 5 channels + + // use listener_forward2d, which doesn't change when player looks up/down. + + Vector listener_forward2d; + + ConvertListenerVectorTo2D( &listener_forward2d, &listener_right ); + + // init struct if 1st time through + + if ( !g_ssp_init ) + { + Q_memset(&g_ssp, 0, sizeof(snd_spatial_t)); + g_ssp_init = true; + } + + // return if dsp_spatial is 0 + + if ( !dsp_spatial.GetInt() ) + return; + + // if listener has not been updated, do nothing + + if ((listener_origin == vec3_origin) && + (listener_forward == vec3_origin) && + (listener_right == vec3_origin) && + (listener_up == vec3_origin) ) + return; + + if ( !SND_IsInGame() ) + return; + + // get time + + double dtime = g_pSoundServices->GetHostTime(); + + // compare to previous time - if starting new check - don't check for new room until timer expires + + if (!g_ssp.chan && !g_ssp.cycle) + { + if (fabs(dtime - g_ssp.last_change) < SND_SPATIAL_WAIT) + return; + } + + // cycle through forward, left, rearward vectors, averaging to get left/right delay + // count[chan][cycle] 0,1 0,2 0,3 1,1 1,2 1,3 2,1 2,2 2,3 ... + + g_ssp.cycle++; + + if (g_ssp.cycle == 3) + { + g_ssp.cycle = 0; + + // cycle through front left, front right, rear left, rear right, front center delays + + g_ssp.chan++; + + if (g_ssp.chan >= chan_max ) + g_ssp.chan = 0; + } + + // set up traceline from player eyes to surrounding walls + + switch( g_ssp.chan ) + { + default: + case 0: // front left: trace max 100' 'cone' to player's left + if ( g_AudioDevice->IsSurround() ) + { + // 4-5 speaker case - front left + v_dir = (-listener_right + listener_forward2d) / 2.0; + v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? -listener_right * 0.5: listener_forward2d * 0.5) : v_dir; + } + else + { + // 2 speaker case - left + v_dir = listener_right * -1.0; + v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_forward2d * 0.5 : -listener_forward2d * 0.5) : v_dir; + v_dir = (v_dir + v_dir2) / 2.0; + } + break; + + case 1: // front right: trace max 100' 'cone' to player's right + if ( g_AudioDevice->IsSurround() ) + { + // 4-5 speaker case - front right + v_dir = (listener_right + listener_forward2d) / 2.0; + v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.5: listener_forward2d * 0.5) : v_dir; + } + else + { + // 2 speaker case - right + v_dir = listener_right; + v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_forward2d * 0.5 : -listener_forward2d * 0.5) : v_dir; + v_dir = (v_dir + v_dir2) / 2.0; + } + break; + + case 2: // rear left: trace max 100' 'cone' to player's rear left + v_dir = (listener_right + listener_forward2d) / -2.0; + v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? -listener_right * 0.5 : -listener_forward2d * 0.5) : v_dir; + break; + + case 3: // rear right: trace max 100' 'cone' to player's rear right + v_dir = (listener_right - listener_forward2d) / 2.0; + v_dir = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.5: -listener_forward2d * 0.5) : v_dir; + break; + + case 4: // front center: trace max 100' 'cone' to player's front + v_dir = listener_forward2d; + v_dir2 = g_ssp.cycle ? (g_ssp.cycle == 1 ? listener_right * 0.15 : -listener_right * 0.15) : v_dir; + v_dir = (v_dir + v_dir2); + break; + } + + Vector endpoint; + trace_t tr; + CTraceFilterWorldOnly filter; + + endpoint = MainViewOrigin() + v_dir * SND_WALL_TRACE_LEN; + Ray_t ray; + ray.Init( MainViewOrigin(), endpoint ); + g_pEngineTraceClient->TraceRay( ray, MASK_BLOCK_AUDIO, &filter, &tr ); + + dist = SND_WALL_TRACE_LEN; + + if ( tr.DidHit() ) + { + dist = VectorLength( tr.endpos - MainViewOrigin() ); + } + + g_ssp.dist[g_ssp.chan][g_ssp.cycle] = dist; + + // set new result in dsp_spatial delay params when all delay values have been filled in + + if (!g_ssp.cycle && !g_ssp.chan) + { + // update delay for each channel + + for (int chan = 0; chan < chan_max; chan++) + { + // compute average of 3 traces per channel + + v = (g_ssp.dist[chan][0] + g_ssp.dist[chan][1] + g_ssp.dist[chan][2]) / 3.0; + vp = g_ssp.value_prev[chan]; + + // only change if 10% difference from previous + + if ((vp != v) && int(v) && (PercentDifference( v, vp ) >= 0.1)) + { + // update when we have data for all L/R && RL/RR channels... + + if (chan & 0x1) + { + float vr = fpmin( v, (50*12.0f) ); + float vl = fpmin(g_ssp.value_prev[chan-1], (50*12.0f)); + +/* UNDONE: not needed, now that this applies only to dsp 'room' buffer + + // ensure minimum separation = average distance to walls + + float dmin = (vl + vr) / 2.0; // average distance to walls + float d = vl - vr; // l/r separation + + // if separation is less than average, increase min + + if (abs(d) < dmin/2) + { + if (vl > vr) + vl += dmin/2 - d; + else + vr += dmin/2 - d; + } +*/ + DSP_SetSpatialDelay(chan-1, vl); + DSP_SetSpatialDelay(chan, vr); + } + + // update center chan + + if (chan == 4) + { + float vl = fpmin( v, (50*12.0f) ); + DSP_SetSpatialDelay(chan, vl); + } + } + + g_ssp.value_prev[chan] = v; + + } + + // update wait timer now that all values have been checked + + g_ssp.last_change = dtime; + } +} + +// Dsp Automatic Selection: + +// a) enabled by setting dsp_room to DSP_AUTOMATIC. Subsequently, dsp_automatic is the actual dsp value for dsp_room. +// b) disabled by setting dsp_room to anything else + +// c) while enabled, detection nodes are placed as player moves into a new space +// i. at each node, a new dsp setting is calculated and dsp_automatic is set to an appropriate preset +// ii. new nodes are set when player moves out of sight of previous node +// iii. moving into line of sight of a detection node causes closest node to player to set dsp_automatic + +// see void DAS_CheckNewRoomDSP( ) for main entrypoint + +ConVar das_debug( "adsp_debug", "0", FCVAR_ARCHIVE ); + // >0: draw blue dsp detection node location + // >1: draw green room trace height detection bars + // 3: draw yellow horizontal trace bars for room width/depth detection + // 4: draw yellow upward traces for height detection + // 5: draw teal box around all props around player + // 6: draw teal box around room as detected + +#define DAS_CWALLS 20 // # of wall traces to save for calculating room dimensions +#define DAS_ROOM_TRACE_LEN (400.0*12.0) // max size of trace to check for room dimensions + +#define DAS_AUTO_WAIT 0.25 // wait min of n seconds between dsp_room changes and update checks + +#define DAS_WIDTH_MIN 0.4 // min % change in avg width of any wall pair to cause new dsp +#define DAS_REFL_MIN 0.5 // min % change in avg refl of any wall to cause new dsp +#define DAS_SKYHIT_MIN 0.8 // min % change in # of sky hits per wall + +#define DAS_DIST_MIN (4.0 * 12.0) // min distance between room dsp changes +#define DAS_DIST_MAX (40.0 * 12.0) // max distance to preserve room dsp changes + +#define DAS_DIST_MIN_OUTSIDE (6.0 * 12.0) // min distance between room dsp changes outside +#define DAS_DIST_MAX_OUTSIDE (100.0 * 12.0) // max distance to preserve room dsp changes outside + +#define IVEC_DIAG_UP 8 // start of diagonal up vectors +#define IVEC_UP 18 // up vector +#define IVEC_DOWN 19 // down vector + +#define DAS_REFLECTIVITY_NORM 0.5 +#define DAS_REFLECTIVITY_SKY 0.0 + +// auto dsp room struct + +struct das_room_t +{ + int dist[DAS_CWALLS]; // distance in units from player to axis aligned and diagonal walls + float reflect[DAS_CWALLS]; // acoustic reflectivity per wall + float skyhits[DAS_CWALLS]; // every sky hit adds 0.1 + Vector hit[DAS_CWALLS]; // location of trace hit on wall - used for calculating average centers + Vector norm[DAS_CWALLS]; // wall normal at hit location + + Vector vplayer; // 'frozen' location above player's head + + Vector vplayer_eyes; // 'frozen' location player's eyes + + int width_max; // max width + int length_max; // max length + int height_max; // max height + + float refl_avg; // running average of reflectivity of all walls + float refl_walls[6]; // left,right,front,back,ceiling,floor reflectivities + + float sky_pct; // percent of sky hits + + Vector room_mins; // room bounds + Vector room_maxs; + + double last_dsp_change; // time since last dsp change + + float diffusion; // 0..1.0 check radius (avg of width_avg) for # of props - scale diffusion based on # found + short iwall; // cycles through walls 0..5, ensuring only one trace per frame + short ent_count; // count of entities found in radius + bool bskyabove; // true if sky found above player (ie: outside) + bool broomready; // true if all distances are filled in and room is ready to check + short lowceiling; // if non-zero, ceiling directly above player if < 112 units +}; + +// dsp detection node + +struct das_node_t +{ + Vector vplayer; // position + + bool fused; // true if valid node + bool fseesplayer; // true if node sees player on last check + short dsp_preset; // preset + + int range_min; // min,max detection ranges + int range_max; + + int dist; // last distance to player + + // room parameters when node was created: + + das_room_t room; +}; + +#define DAS_CNODES 40 // keep around last n nodes - must be same as DSP_CAUTO_PRESETS!!! + +das_node_t g_das_nodes[DAS_CNODES]; // all dsp detection nodes +das_node_t *g_pdas_last_node = NULL; // last node that saw player + +int g_das_check_next; // next node to check +int g_das_store_next; // next place to store node +bool g_das_all_checked; // true if all nodes checked +int g_das_checked_count; // count of nodes checked in latest pass + +das_room_t g_das_room; // room detector + +bool g_bdas_room_init = 0; +bool g_bdas_init_nodes = 0; +bool g_bdas_create_new_node = 0; + +bool DAS_TraceNodeToPlayer( das_room_t *proom, das_node_t *pnode ); +void DAS_InitAutoRoom( das_room_t *proom); +void DAS_DebugDrawTrace ( trace_t *ptr, int r, int g, int b, float duration, int imax ); + +Vector g_das_vec3[DAS_CWALLS]; // trace vectors to walls, ceiling, floor + +void DAS_InitNodes( void ) +{ + Q_memset(g_das_nodes, 0, sizeof(das_node_t) * DAS_CNODES); + g_das_check_next = 0; + g_das_store_next = 0; + g_das_all_checked = 0; + g_das_checked_count = 0; + + // init all rooms + + for (int i = 0; i < DAS_CNODES; i++) + DAS_InitAutoRoom( &(g_das_nodes[i].room) ); + + // init trace vectors + // set up trace vectors for max, min width + float vl = DAS_ROOM_TRACE_LEN; + float vlu = DAS_ROOM_TRACE_LEN * 0.52; + float vlu2 = DAS_ROOM_TRACE_LEN * 0.48; // don't use 'perfect' diagonals + + g_das_vec3[0].Init(vl, 0.0, 0.0); // x left + g_das_vec3[1].Init(-vl, 0.0, 0.0); // x right + + g_das_vec3[2].Init(0.0, vl, 0.0); // y front + g_das_vec3[3].Init(0.0, -vl, 0.0); // y back + + g_das_vec3[4].Init(-vlu, vlu2, 0.0); // diagonal front left + g_das_vec3[5].Init(vlu, -vlu2, 0.0); // diagonal rear right + + g_das_vec3[6].Init(vlu, vlu2, 0.0); // diagonal front right + g_das_vec3[7].Init(-vlu, -vlu2, 0.0); // diagonal rear left + + // set up trace vectors for max height - on x=y diagonal + + g_das_vec3[8].Init(vlu, vlu2, vlu/2.0); // front right up A x,y,z/2 (IVEC_DIAG_UP) + g_das_vec3[9].Init(vlu, vlu2, vlu); // front right up B x,y,z + g_das_vec3[10].Init(vlu/2.0, vlu2/2.0, vlu); // front right up C x/2,y/2,z + + g_das_vec3[11].Init(-vlu, -vlu2, vlu/2.0); // rear left up A -x,-y,z/2 + g_das_vec3[12].Init(-vlu, -vlu2, vlu); // rear left up B -x,-y,z + g_das_vec3[13].Init(-vlu/2.0, -vlu2/2.0, vlu); // rear left up C -x/2,-y/2,z + + // set up trace vectors for max height - on x axis & y axis + + g_das_vec3[14].Init(-vlu, 0, vlu); // left up B -x,0,z + g_das_vec3[15].Init(0, vlu/2.0, vlu); // front up C -x/2,0,z + + g_das_vec3[16].Init(0, -vlu, vlu); // rear up B x,0,z + g_das_vec3[17].Init(vlu/2.0, 0, vlu); // right up C x/2,0,z + + g_das_vec3[18].Init(0.0, 0.0, vl); // up (IVEC_UP) + g_das_vec3[19].Init(0.0, 0.0, -vl); // down (IVEC_DOWN) +} + +void DAS_InitAutoRoom( das_room_t *proom) +{ + Q_memset(proom, 0, sizeof (das_room_t)); +} + +// reset all nodes for next round of visibility checks between player & nodes + +void DAS_ResetNodes( void ) +{ + for (int i = 0; i < DAS_CNODES; i++) + { + g_das_nodes[i].fseesplayer = false; + g_das_nodes[i].dist = 0; + } + + g_das_all_checked = false; + g_das_checked_count = 0; + g_bdas_create_new_node = false; +} + +// utility function - return next index, wrap at max + +int DAS_GetNextIndex( int *pindex, int max ) +{ + int i = *pindex; + int j; + + j = i+1; + if ( j >= max ) + j = 0; + + *pindex = j; + + return i; +} + +// returns true if dsp node is within range of player + +bool DAS_NodeInRange( das_room_t *proom, das_node_t *pnode ) +{ + float dist; + + dist = VectorLength( proom->vplayer - pnode->vplayer ); + + // player can still see previous room selection point, and it's less than n feet away, + // then flag this node as visible + + pnode->dist = dist; + + return ( dist <= pnode->range_max ); +} + +// update next valid node - set up internal node state if it can see player +// called once per frame +// returns true if all nodes have been checked + +bool DAS_CheckNextNode( das_room_t *proom ) +{ + int i, j; + + if ( g_das_all_checked ) + return true; + + // find next valid node + + for (j = 0; j < DAS_CNODES; j++) + { + // track number of nodes checked + + g_das_checked_count++; + + // get next node in range to check + + i = DAS_GetNextIndex( &g_das_check_next, DAS_CNODES ); + + if ( g_das_nodes[i].fused && DAS_NodeInRange( proom, &(g_das_nodes[i]) ) ) + { + // trace to see if player can still see node, + // if so stop checking + + if ( DAS_TraceNodeToPlayer( proom, &(g_das_nodes[i]) )) + goto checknode_exit; + } + } + +checknode_exit: + + // flag that all nodes have been checked + + if ( g_das_checked_count >= DAS_CNODES ) + g_das_all_checked = true; + + return g_das_all_checked; +} + + +int DAS_GetNextNodeIndex() +{ + return g_das_store_next; +} +// store new node for room + +void DAS_StoreNode( das_room_t *proom, int dsp_preset) +{ + // overwrite node in cyclic list + + int i = DAS_GetNextIndex( &g_das_store_next, DAS_CNODES ); + + g_das_nodes[i].dsp_preset = dsp_preset; + g_das_nodes[i].fused = true; + g_das_nodes[i].vplayer = proom->vplayer; + + // calculate node scanning range_max based on room size + + if ( !proom->bskyabove ) + { + // inside range - halls & tunnels have nodes every 5*width + g_das_nodes[i].range_max = fpmin((int)DAS_DIST_MAX, min(proom->width_max * 5, proom->length_max) ); + g_das_nodes[i].range_min = DAS_DIST_MIN; + } + else + { + // outside range + g_das_nodes[i].range_max = DAS_DIST_MAX_OUTSIDE; + g_das_nodes[i].range_min = DAS_DIST_MIN_OUTSIDE; + } + + g_das_nodes[i].fseesplayer = false; + g_das_nodes[i].dist = 0; + + g_das_nodes[i].room = *proom; + + // update last node visible as this node + + g_pdas_last_node = &(g_das_nodes[i]); +} + +// check all updated nodes, +// return dsp_preset of largest node (by area) that can see player +// return -1 if no preset found + +// NOTE: outside nodes can't see player if player is inside and vice versa +// foutside is true if player is outside + +int DAS_GetDspPreset( bool foutside ) +{ + int dsp_preset = -1; + + int i; + // int dist_min = 100000; + int area_max = 0; + int area; + + // find node that represents room with greatest floor area, return its preset. + + for (i = 0; i < DAS_CNODES; i++) + { + if (g_das_nodes[i].fused && g_das_nodes[i].fseesplayer) + { + area = (g_das_nodes[i].room.width_max * g_das_nodes[i].room.length_max); + + if ( g_das_nodes[i].room.bskyabove == foutside ) + { + if (area > area_max) + { + area_max = area; + dsp_preset = g_das_nodes[i].dsp_preset; + + // save pointer to last node that saw player + + g_pdas_last_node = &(g_das_nodes[i]); + } + } +/* + + // find nearest node, return its preset + + if (g_das_nodes[i].dist < dist_min) + { + if ( g_das_nodes[i].room.bskyabove == foutside ) + { + dist_min = g_das_nodes[i].dist; + dsp_preset = g_das_nodes[i].dsp_preset; + + // save pointer to last node that saw player + + g_pdas_last_node = &(g_das_nodes[i]); + + } + } +*/ + } + } + + return dsp_preset; +} + +// custom trace filter: +// a) never hit player or monsters or entities +// b) always hit world, or moveables or static props + +class CTraceFilterDAS : public ITraceFilter +{ +public: + bool ShouldHitEntity( IHandleEntity *pHandleEntity, int contentsMask ) + { + IClientUnknown *pUnk = static_cast<IClientUnknown*>(pHandleEntity); + IClientEntity *pEntity; + + if ( !pUnk ) + return false; + + // don't hit non-collideable props + + if ( StaticPropMgr()->IsStaticProp( pHandleEntity ) ) + { + + ICollideable *pCollide = StaticPropMgr()->GetStaticProp( pHandleEntity); + if (!pCollide) + return false; + } + + // don't hit any ents + + pEntity = pUnk->GetIClientEntity(); + + if ( pEntity ) + return false; + + return true; + } + + virtual TraceType_t GetTraceType() const + { + return TRACE_EVERYTHING_FILTER_PROPS; + } +}; + +#define DAS_TRACE_MASK (CONTENTS_SOLID|CONTENTS_MOVEABLE|CONTENTS_WINDOW) + +// returns true if clear line exists between node and player +// if node can see player, sets up node distance and flag fseesplayer + +bool DAS_TraceNodeToPlayer( das_room_t *proom, das_node_t *pnode ) +{ + trace_t trP; + CTraceFilterDAS filterP; + bool fseesplayer = false; + float dist; + Ray_t ray; + ray.Init( proom->vplayer, pnode->vplayer ); + + g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterP, &trP ); + dist = VectorLength( proom->vplayer - pnode->vplayer ); + + // player can still see previous room selection point, and it's less than n feet away, + // then flag this node as visible + + if ( !trP.DidHit() && (dist <= DAS_DIST_MAX) ) + { + fseesplayer = true; + pnode->dist = dist; + } + + pnode->fseesplayer = fseesplayer; + + return fseesplayer; +} + +// update room boundary maxs, mins + +void DAS_SetRoomBounds( das_room_t *proom, Vector &hit, bool bheight ) +{ + Vector maxs, mins; + + maxs = proom->room_maxs; + mins = proom->room_mins; + + if (!bheight) + { + if (hit.x > maxs.x) + maxs.x = hit.x; + + if (hit.x < mins.x) + mins.x = hit.x; + + if (hit.z > maxs.z) + maxs.z = hit.z; + + if (hit.z < mins.z) + mins.z = hit.z; + } + + if (bheight) + { + if (hit.y > maxs.y) + maxs.y = hit.y; + + if (hit.y < mins.y) + mins.y = hit.y; + } + + proom->room_maxs = maxs; + proom->room_mins = mins; +} + +// when all walls are updated, calculate max length, width, height, reflectivity, sky hit%, room center +// returns true if room parameters are in good location to place a node +// returns false if room parameters are not in good location to place a node +// note: false occurs if up vector doesn't hit sky, but one or more up diagonal vectors do hit sky + +bool DAS_CalcRoomProps( das_room_t *proom ) +{ + int length_max = 0; + int width_max = 0; + int height_max = 0; + int dist[4]; + float area1, area2; + int height; + int i; + int j; + int k; + bool b_diaghitsky = false; + + // reject this location if up vector doesn't hit sky, but + // one or more up diagonals do hit sky - + // in this case, player is under a slight overhang, narrow bridge, or + // standing just inside a window or doorway. keep looking for better node location + + for (i = IVEC_DIAG_UP; i < IVEC_UP; i++) + { + if (proom->skyhits[i] > 0.0) + b_diaghitsky = true; + } + + if (b_diaghitsky && !(proom->skyhits[IVEC_UP] > 0.0)) + return false; + + // get all distance pairs + + for (i = 0; i < IVEC_DIAG_UP; i+=2) + dist[i/2] = proom->dist[i] + proom->dist[i+1]; // 1st pair is width + + // if areas differ by more than 25% + // select the pair with the greater area + + // if areas do not differ by more than 25%, select the pair with the + // longer measured distance. Filters incorrect selection due to diagonals. + + area1 = (float)(dist[0] * dist[1]); + area2 = (float)(dist[2] * dist[3]); + + area1 = (int)area1 == 0 ? 1.0 : area1; + area2 = (int)area2 == 0 ? 1.0 : area2; + + if ( PercentDifference(area1, area2) > 0.25 ) + { + // areas are more than 25% different - select pair with greater area + + j = area1 > area2 ? 0 : 2; + } + else + { + // select pair with longer measured distance + + int iMaxDist = 0; // index to max dist + int dmax = 0; + + for (i = 0; i < 4; i++) + { + if (dist[i] > dmax) + { + dmax = dist[i]; + iMaxDist = i; + } + } + + j = iMaxDist > 1 ? 2 : 0; + } + + + // width is always the smaller of the dimensions + + width_max = min (dist[j], dist[j+1]); + length_max = max (dist[j], dist[j+1]); + + // get max height + + for (i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) + { + height = proom->dist[i]; + + if (height > height_max) + height_max = height; + } + + proom->length_max = length_max; + proom->width_max = width_max; + proom->height_max = height_max; + + // get room max,min from chosen width, depth + // 0..3 or 4..7 + + for ( i = j*2; i < 4+(j*2); i++) + DAS_SetRoomBounds( proom, proom->hit[i], false ); + + // get room height min from down trace + + proom->room_mins.z = proom->hit[IVEC_DOWN].z; + + // reset room height max to player trace height + + proom->room_maxs.z = proom->vplayer.z; + + // draw box around room max,min + + if (das_debug.GetInt() == 6) + { + // draw box around all objects detected + Vector maxs = proom->room_maxs; + Vector mins = proom->room_mins; + Vector orig = (maxs + mins) / 2.0; + Vector absMax = maxs - orig; + Vector absMin = mins - orig; + + CDebugOverlay::AddBoxOverlay( orig, absMax, absMin, vec3_angle, 255, 0, 255, 0, 60.0f ); + } + // calculate average reflectivity + + float refl = 0.0; + + // average reflectivity for walls + + // 0..3 or 4..7 + + for ( k = 0, i = j*2; i < 4+(j*2); i++, k++) + { + refl += proom->reflect[i]; + proom->refl_walls[k] = proom->reflect[i]; + } + + // assume ceiling is open + + proom->refl_walls[4] = 0.0; + + // get ceiling reflectivity, if any non zero + + for ( i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) + { + if (proom->reflect[i] == 0.0) + { + // if any upward trace hit sky, exit; + // ceiling reflectivity is 0.0 + + proom->refl_walls[4] = 0.0; + + i = IVEC_DOWN; // exit loop + } + else + { + + // upward trace didn't hit sky, keep checking + + proom->refl_walls[4] = proom->reflect[i]; + } + } + + // add in ceiling reflectivity, if any + + refl += proom->refl_walls[4]; + + // get floor reflectivity + + refl += proom->reflect[IVEC_DOWN]; + proom->refl_walls[5] = proom->reflect[IVEC_DOWN]; + + proom->refl_avg = refl / 6.0; + + // calculate sky hit percent for this wall + + float sky_pct = 0.0; + + // 0..3 or 4..7 + + for ( i = j*2; i < 4+(j*2); i++) + sky_pct += proom->skyhits[i]; + + for ( i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) + { + if (proom->skyhits[i] > 0.0) + { + // if any upward trace hit sky, exit loop + sky_pct += proom->skyhits[i]; + i = IVEC_DOWN; + } + } + + // get floor skyhit + + sky_pct += proom->skyhits[IVEC_DOWN]; + + proom->sky_pct = sky_pct; + + // check for sky above + proom->bskyabove = false; + + for (i = IVEC_DIAG_UP; i < IVEC_DOWN; i++) + { + if (proom->skyhits[i] > 0.0) + proom->bskyabove = true; + } + + return true; +} + +// return true if trace hit solid +// return false if trace hit sky or didn't hit anything + +bool DAS_HitSolid( trace_t *ptr ) +{ + // if hit nothing return false + + if (!ptr->DidHit()) + return false; + + // if hit sky, return false (not solid) + if (ptr->surface.flags & SURF_SKY) + return false; + + return true; +} + +// returns true if trace hit sky + +bool DAS_HitSky( trace_t *ptr ) +{ + if (ptr->DidHit() && (ptr->surface.flags & SURF_SKY)) + return true; + if (!ptr->DidHit() ) + { + float dz = ptr->endpos.z - ptr->startpos.z; + if ( dz > 200*12.0f ) + return true; + } + return false; +} + + +bool DAS_ScanningForHeight( das_room_t *proom ) +{ + return (proom->iwall >= IVEC_DIAG_UP); +} + +bool DAS_ScanningForWidth( das_room_t *proom ) +{ + return (proom->iwall < IVEC_DIAG_UP); +} + +bool DAS_ScanningForFloor( das_room_t *proom ) +{ + return (proom->iwall == IVEC_DOWN); +} + +ConVar das_door_height("adsp_door_height", "112"); // standard door height hl2 +ConVar das_wall_height("adsp_wall_height", "128"); // standard wall height hl2 +ConVar das_low_ceiling("adsp_low_ceiling", "108"); // low ceiling height hl2 + + +// set origin for tracing out to walls to point above player's head +// allows calculations over walls and floor obstacles, and above door openings + +// WARNING: the current settings are optimal for skipping floor and ceiling clutter, +// and for detecting rooms without 'looking' through doors or windows. Don't change these cvars for hl2! + +void DAS_SetTraceHeight( das_room_t *proom, trace_t *ptrU, trace_t *ptrD ) +{ + // NOTE: when tracing down through player's box, endpos and startpos are reversed and + // startsolid and allsolid are true. + + int zup = abs(ptrU->endpos.z - ptrU->startpos.z); // height above player's head + int zdown = abs(ptrD->endpos.z - ptrD->startpos.z); // distance to floor from player's head + int h; + h = zup + zdown; + + int door_height = das_door_height.GetInt(); + int wall_height = das_wall_height.GetInt(); + int low_ceiling = das_low_ceiling.GetInt(); + + if (h > low_ceiling && h <= wall_height) + { + // low ceiling - trace out just above standard door height @ 112 + if (h > door_height) + proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + door_height + 1; + else + proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + h - 1; + } + else if ( h > wall_height ) + { + // tall ceiling - trace out over standard walls @ 128 + + proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + wall_height + 1; + } + else + { + // very low ceiling, trace out from just below ceiling + proom->vplayer.z = fpmin(ptrD->endpos.z, ptrD->startpos.z) + h - 1; + proom->lowceiling = h; + } + + Assert (proom->vplayer.z <= ptrU->endpos.z); + + if (das_debug.GetInt() > 1) + { + // draw line to height, and between floor and ceiling + + CDebugOverlay::AddLineOverlay( ptrD->endpos, ptrU->endpos, 0, 255, 0, 255, false, 20 ); + + Vector mins; + Vector maxs; + mins.Init(-1,-1,-2.0); + maxs.Init(1,1,0); + + CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 255, 0, 0, 0, 20 ); + + CDebugOverlay::AddBoxOverlay( ptrU->endpos, mins, maxs, vec3_angle, 0, 255, 0, 0, 20 ); + CDebugOverlay::AddBoxOverlay( ptrD->endpos, mins, maxs, vec3_angle, 0, 255, 0, 0, 20 ); + + } +} + +// prepare room struct for new round of checks: +// clear out struct, +// init trace height origin by finding space above player's head +// returns true if player is in valid position to begin checks from + +bool DAS_StartTraceChecks( das_room_t *proom ) +{ + // starting new check: store player position, init maxs, mins + + proom->vplayer_eyes = MainViewOrigin(); + proom->vplayer = MainViewOrigin(); + + proom->height_max = 0; + proom->width_max = 0; + proom->length_max = 0; + proom->room_maxs.Init (0.0, 0.0, 0.0); + proom->room_mins.Init (10000.0, 10000.0, 10000.0); + + proom->lowceiling = 0; + + // find point between player's head and ceiling - trace out to walls from here + + trace_t trU, trD; + CTraceFilterDAS filterU, filterD; + + Vector v_dir = g_das_vec3[IVEC_DOWN]; // down - find floor + + Vector endpoint = proom->vplayer + v_dir; + + Ray_t ray; + ray.Init( proom->vplayer, endpoint ); + + g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterD, &trD ); + + // if player jumping or in air, don't continue + + if (trD.DidHit() && abs(trD.endpos.z - trD.startpos.z) > 72) + return false; + + v_dir = g_das_vec3[IVEC_UP]; // up - find ceiling + + endpoint = proom->vplayer + v_dir; + + ray.Init( proom->vplayer, endpoint ); + + g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterU, &trU ); + + // if down trace hits floor, set trace height, otherwise default is player eye location + + if ( DAS_HitSolid( &trD) ) + DAS_SetTraceHeight( proom, &trU, &trD ); + + return true; +} + +void DAS_DebugDrawTrace ( trace_t *ptr, int r, int g, int b, float duration, int imax) +{ + + // das_debug == 3: draw horizontal trace bars for room width/depth detection + // das_debug == 4: draw upward traces for height detection + + if (das_debug.GetInt() != imax) + return; + + CDebugOverlay::AddLineOverlay( ptr->startpos, ptr->endpos, r, g, b, 255, false, duration ); + + Vector mins; + Vector maxs; + mins.Init(-1,-1,-2.0); + maxs.Init(1,1,0); + + CDebugOverlay::AddBoxOverlay( ptr->endpos, mins, maxs, vec3_angle, r, g, b, 0, duration ); + +} + +// wall surface data + +struct das_surfdata_t +{ + float dist; // distance to player + float reflectivity; // acoustic reflectivity of material on surface + Vector hit; // trace hit location + Vector norm; // wall normal at hit location +}; + +// trace hit wall surface, get info about surface and store in surfdata struct +// if scanning for height, bounce a second trace off of ceiling and get dist to floor + +void DAS_GetSurfaceData( das_room_t *proom, trace_t *ptr, das_surfdata_t *psurfdata ) +{ + + float dist; // distance to player + float reflectivity; // acoustic reflectivity of material on surface + Vector hit; // trace hit location + Vector norm; // wall normal at hit location + surfacedata_t *psurf; + + psurf = physprop->GetSurfaceData( ptr->surface.surfaceProps ); + + reflectivity = psurf ? psurf->audio.reflectivity : DAS_REFLECTIVITY_NORM; + + // keep wall hit location and normal, to calc room bounds and center + + norm = ptr->plane.normal; + + // get length to hit location + + dist = VectorLength(ptr->endpos - ptr->startpos); + + // if started tracing from within player box, startpos & endpos may be flipped + + if (ptr->endpos.z >= ptr->startpos.z) + hit = ptr->endpos; + else + hit = ptr->startpos; + + // if checking for max height by bouncing several vectors off of ceiling: + // ignore returned normal from 1st bounce, just search straight down from trace hit location + + if ( DAS_ScanningForHeight( proom ) && !DAS_ScanningForFloor( proom ) ) + { + trace_t tr2; + CTraceFilterDAS filter2; + + norm.Init(0.0, 0.0, -1.0); + + Vector endpoint = hit + ( norm * DAS_ROOM_TRACE_LEN ); + + Ray_t ray; + ray.Init( hit, endpoint ); + + g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filter2, &tr2 ); + + //DAS_DebugDrawTrace( &tr2, 255, 255, 0, 10, 1); + + if (tr2.DidHit()) + { + // get distance between surfaces + + dist = VectorLength(tr2.endpos - tr2.startpos); + } + } + + // set up surface struct and return + + psurfdata->dist = dist; + psurfdata->hit = hit; + psurfdata->norm = norm; + psurfdata->reflectivity = reflectivity; + +} + + +// algorithm for detecting approximate size of space around player. Handles player in corner & non-axis aligned rooms. +// also handles player on catwalk or player under small bridge/overhang. +// The goal is to only change the dsp room description if the the player moves into +// a space which is SIGNIFICANTLY different from the previously set dsp space. + +// save player position. find a point above player's head and trace out from here. + +// from player position, get max width and max length: + +// from player position, +// a) trace x,-x, y,-y axes +// b) trace xy, -xy, x-y, -x-y diagonals +// c) select largest room size detected from max width, max length + + +// from player position, get height +// a) trace out along front-up (or left-up, back-up, right-up), save hit locations +// b) trace down -z from hit locations +// c) save max height + +// when max width, max length, max height all updated, get new player position + +// get average room size & wall materials: +// update averages with one traceline per frame only +// returns true if room is fully updated and ready to check + +bool DAS_UpdateRoomSize( das_room_t *proom ) +{ + Vector endpoint; + Vector startpoint; + Vector v_dir; + int iwall; + bool bskyhit = false; + das_surfdata_t surfdata; + + // do nothing if room already fully checked + + if ( proom->broomready ) + return true; + + // cycle through all walls, floor, ceiling + // get wall index + + iwall = proom->iwall; + + // get height above player and init proom for new round of checks + + if (iwall == 0) + { + if (!DAS_StartTraceChecks( proom )) + return false; // bad location to check room - player is jumping etc. + } + + // get trace vector + + v_dir = g_das_vec3[iwall]; + + // trace out from trace origin, in axis-aligned direction or along diagonals + + // if looking for max height, trace from top of player's eyes + + if ( DAS_ScanningForHeight( proom ) ) + { + startpoint = proom->vplayer_eyes; + endpoint = proom->vplayer_eyes + v_dir; + } + else + { + startpoint = proom->vplayer; + endpoint = proom->vplayer + v_dir; + } + + // try less expensive world-only trace first (no props, no ents - just try to hit walls) + + trace_t tr; + CTraceFilterWorldOnly filter; + + Ray_t ray; + ray.Init( startpoint, endpoint ); + + g_pEngineTraceClient->TraceRay( ray, CONTENTS_SOLID, &filter, &tr ); + + // if didn't hit world, or we hit sky when looking horizontally, + // retrace, this time including props + + if ( !DAS_HitSolid( &tr ) && DAS_ScanningForWidth( proom ) ) + { + CTraceFilterDAS filterDas; + + ray.Init( startpoint, endpoint ); + g_pEngineTraceClient->TraceRay( ray, DAS_TRACE_MASK, &filterDas, &tr ); + } + + if (das_debug.GetInt() > 2) + { + // draw trace lines + + if ( DAS_HitSolid( &tr ) ) + DAS_DebugDrawTrace( &tr, 0, 255, 255, 10, DAS_ScanningForHeight( proom ) + 3); + else + DAS_DebugDrawTrace( &tr, 255, 0, 0, 10, DAS_ScanningForHeight( proom ) + 3); // red lines if sky hit or no hit + } + + // init surface data with defaults, in case we didn't hit world + + surfdata.dist = DAS_ROOM_TRACE_LEN; + surfdata.reflectivity = DAS_REFLECTIVITY_SKY; // assume sky or open area + surfdata.hit = endpoint; // trace hit location + surfdata.norm = -v_dir; + + // check for sky hits + + if ( DAS_HitSky( &tr ) ) + { + bskyhit = true; + + if ( DAS_ScanningForWidth( proom ) ) + // ignore horizontal sky hits for distance calculations + surfdata.dist = 1.0; + else + surfdata.dist = surfdata.dist; // debug + } + + // get length of trace if it hit world + + // if hit solid and not sky (tr.DidHit() && !bskyhit) + // get surface information + + if ( DAS_HitSolid( &tr) ) + DAS_GetSurfaceData( proom, &tr, &surfdata ); + + // store surface data + + proom->dist[iwall] = surfdata.dist; + proom->reflect[iwall] = clamp(surfdata.reflectivity, 0.0f, 1.0f); + proom->skyhits[iwall] = bskyhit ? 0.1 : 0.0; + proom->hit[iwall] = surfdata.hit; + proom->norm[iwall] = surfdata.norm; + + // update wall counter + + proom->iwall++; + + if (proom->iwall == DAS_CWALLS) + { + bool b_good_node_location; + + // calculate room mins, maxs, reflectivity etc + + b_good_node_location = DAS_CalcRoomProps( proom ); + + // reset wall counter + + proom->iwall = 0; + proom->broomready = b_good_node_location; // room ready to check if good node location + + return b_good_node_location; + } + + return false; // room not yet fully updated +} + +// create entity enumerator for counting ents & summing volume of ents in room + +class CDasEntEnum : public IPartitionEnumerator +{ + public: + int m_count; // # of ents in space + float m_volume; // space occupied by ents + + public: + + void Reset() + { + m_count = 0; + m_volume = 0.0; + } + + // called with each handle... + + IterationRetval_t EnumElement( IHandleEntity *pHandleEntity ) + { + float vol; + + // get bounding box of entity + // Generate a collideable + + ICollideable *pCollideable = g_pEngineTraceClient->GetCollideable( pHandleEntity ); + + if ( !pCollideable ) + return ITERATION_CONTINUE; + + // Check for solid + + if ( !IsSolid( pCollideable->GetSolid(), pCollideable->GetSolidFlags() ) ) + return ITERATION_CONTINUE; + + m_count++; + + // compute volume of space occupied by entity + Vector mins = pCollideable->OBBMins(); + Vector maxs = pCollideable->OBBMaxs(); + + vol = fabs((maxs.x - mins.x) * (maxs.y - mins.y) * (maxs.z - mins.z)); + + m_volume += vol; // add to total vol + + if (das_debug.GetInt() == 5) + { + // draw box around all objects detected + + Vector orig = pCollideable->GetCollisionOrigin(); + CDebugOverlay::AddBoxOverlay( orig, mins, maxs, pCollideable->GetCollisionAngles(), 255, 0, 255, 0, 60.0f ); + } + + return ITERATION_CONTINUE; + } +}; + +// determine # of solid ents/props within detected room boundaries +// and set diffusion based on count of ents and spatial volume of ents + +void DAS_SetDiffusion( das_room_t *proom ) +{ + // BRJ 7/12/05 + // This was commented out because the y component of proom->room_mins, proom->room_maxs was never + // being computed, causing a bogus box to be sent to the partition system. The results of + // this computation (namely the diffusion + ent_count fields of das_room_t) were never being used. + // Therefore, we'll avoid the enumeration altogether + + proom->diffusion = 0.0f; + proom->ent_count = 0; + + /* + CDasEntEnum enumerator; + SpatialPartitionListMask_t mask = PARTITION_CLIENT_SOLID_EDICTS; // count only solid ents in room + int count; + float vol; + float volroom; + float dfn; + + enumerator.Reset(); + + SpatialPartition()->EnumerateElementsInBox(mask, proom->room_mins, proom->room_maxs, true, &enumerator ); + + count = enumerator.m_count; + vol = enumerator.m_volume; + + // compute diffusion from volume + + // how much space around player is filled with props? + + volroom = (proom->room_maxs.x - proom->room_mins.x) * (proom->room_maxs.y - proom->room_mins.y) * (proom->room_maxs.z - proom->room_mins.z); + volroom = fabs(volroom); + + if ( !(int)volroom ) + volroom = 1.0; + + dfn = vol / volroom; // % of total volume occupied by props + + dfn = clamp (dfn, 0.0, 1.0); + + proom->diffusion = dfn; + proom->ent_count = count; + */ +} + +// debug routine to display current room params + +void DAS_DisplayRoomDEBUG( das_room_t *proom, bool fnew, float preset ) +{ + float dx,dy,dz; + Vector ctr; + float count; + + if (das_debug.GetInt() == 0) + return; + + dx = proom->length_max / 12.0; + dy = proom->width_max / 12.0; + dz = proom->height_max / 12.0; + + float refl = proom->refl_avg; + + count = (float)(proom->ent_count); + float fsky = (proom->bskyabove ? 1.0 : 0.0); + + if (fnew) + DevMsg( "NEW DSP NODE: size:(%.0f,%.0f) height:(%.0f) dif %.4f : refl %.4f : cobj: %.0f : sky %.0f \n", dx, dy, dz, proom->diffusion, refl, count, fsky); + + if (!fnew && preset < 0.0) + return; + + if (preset >= 0.0) + { + if (proom == NULL) + return; + + DevMsg( "DSP PRESET: %.0f size:(%.0f,%.0f) height:(%.0f) dif %.4f : refl %.4f : cobj: %.0f : sky %.0f \n", preset, dx, dy, dz, proom->diffusion, refl, count, fsky); + return; + } + + // draw box around new node location + + Vector mins; + Vector maxs; + mins.Init(-8,-8,-16); + maxs.Init(8,8,0); + + CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 0, 0, 255, 0, 1000.0f ); + + // draw red box around node origin + + mins.Init(-0.5,-0.5,-1.0); + maxs.Init(0.5,0.5,0); + + CDebugOverlay::AddBoxOverlay( proom->vplayer, mins, maxs, vec3_angle, 255, 0, 0, 0, 1000.0f ); + + CDebugOverlay::AddTextOverlay( proom->vplayer, 0, 10, 1.0, "DSP NODE" ); +} + +// check newly calculated room parameters against current stored params. +// if different, return true. +// NOTE: only call when all proom params have been calculated. +// return false if this is not a good location for creating a new node + +bool DAS_CheckNewRoom( das_room_t *proom ) +{ + bool bnewroom; + float dw,dw2,dr,ds,dh; + int cchanged = 0; + das_room_t *proom_prev = NULL; + Vector2D v2d; + Vector v3d; + float dist; + + // player can't see previous node, determine if this is a good place to lay down + // a new node. Get room at last seen node for comparison + + if (g_pdas_last_node) + proom_prev = &(g_pdas_last_node->room); + + // no previous room node saw player, go create new room node + + if (!proom_prev) + { + bnewroom = true; + goto check_ret; + } + + // if player not at least n feet from last node, return false + + v3d = proom->vplayer - proom_prev->vplayer; + v2d.Init(v3d.x, v3d.y); + + dist = Vector2DLength(v2d); + + if (dist <= DAS_DIST_MIN) + return false; + + // see if room size has changed significantly since last node + + bnewroom = true; + + dw = 0.0; + dw2 = 0.0; + dh = 0.0; + dr = 0.0; + + if ( proom_prev->width_max != 0 ) + dw = (float)proom->width_max / (float)proom_prev->width_max; // max width delta + + if ( proom_prev->length_max != 0 ) + dw2 = (float)proom->length_max / (float)proom_prev->length_max; // max length delta + + if ( proom_prev->height_max != 0 ) + dh = (float)proom->height_max / (float)proom_prev->height_max; // max height delta + + if ( proom_prev->refl_avg != 0.0 ) + dr = proom->refl_avg / proom_prev->refl_avg; // reflectivity delta + + ds = fabs( proom->sky_pct - proom_prev->sky_pct); // sky hits delta + + if (dw > 1.0) dw = 1.0 / dw; + if (dw2 > 1.0) dw = 1.0 / dw2; + if (dh > 1.0) dh = 1.0 / dh; + if (dr > 1.0) dr = 1.0 / dr; + + if ( (1.0 - dw) >= DAS_WIDTH_MIN ) + cchanged++; + + if ( (1.0 - dw2) >= DAS_WIDTH_MIN ) + cchanged++; + +// if ( (1.0 - dh) >= DAS_WIDTH_MIN ) // don't change room based on height change +// cchanged++; + + // new room only if at least 1 changed + + if (cchanged >= 1) + goto check_ret; + +// if ( (1.0 - dr) >= DAS_REFL_MIN ) // don't change room based on reflectivity change +// goto check_ret; + +// if (ds >= DAS_SKYHIT_MIN ) +// goto check_ret; + + // new room if sky above changes state + + if (proom->bskyabove != proom_prev->bskyabove) + goto check_ret; + + // room didn't change significantly, return false + + bnewroom = false; + +check_ret: + + if ( bnewroom ) + { + // if low ceiling detected < 112 units, and max height is > low ceiling height by 20%, discard - no change + // this detects player in doorway, under pipe or narrow bridge + + if ( proom->lowceiling && (proom->lowceiling < proom->height_max)) + { + float h = (float)(proom->lowceiling) / (float)proom->height_max; + + if (h < 0.8) + return false; + } + + DAS_SetDiffusion( proom ); + } + + DAS_DisplayRoomDEBUG( proom, bnewroom, -1.0 ); + + return bnewroom; +} + + +extern int DSP_ConstructPreset( bool bskyabove, int width, int length, int height, float fdiffusion, float freflectivity, float *psurf_refl, int inode, int cnodes ); + +// select new dsp_room based on size, wall materials +// (or modulate params for current dsp) +// returns new preset # for dsp_automatic + +int DAS_GetRoomDSP( das_room_t *proom, int inode ) +{ + + // preset constructor + // call dsp module with params, get dsp preset back + + bool bskyabove = proom->bskyabove; + int width = proom->width_max; + int length = proom->length_max; + int height = proom->height_max; + float fdiffusion = proom->diffusion; + float freflectivity = proom->refl_avg; + float surf_refl[6]; + + // fill array of surface reflectivities - for left,right,front,back,ceiling,floor + + for (int i = 0; i < 6; i++) + surf_refl[i] = proom->refl_walls[i]; + + return DSP_ConstructPreset( bskyabove, width, length, height, fdiffusion, freflectivity, surf_refl, inode, DAS_CNODES ); + +} + + +// main entry point: call once per frame to update dsp_automatic +// for automatic room detection. dsp_room must be set to DSP_AUTOMATIC to enable. +// NOTE: this routine accumulates traceline information over several frames - it +// never traces more than 3 times per call, and normally just once per call. + +void DAS_CheckNewRoomDSP( ) +{ + VPROF("DAS_CheckNewRoomDSP"); + das_room_t *proom = &g_das_room; + int dsp_preset; + bool bRoom_ready = false; + + // if listener has not been updated, do nothing + + if ((listener_origin == vec3_origin) && + (listener_forward == vec3_origin) && + (listener_right == vec3_origin) && + (listener_up == vec3_origin) ) + return; + + if ( !SND_IsInGame() ) + return; + + // make sure we init nodes & vectors first time this is called + + if ( !g_bdas_init_nodes ) + { + g_bdas_init_nodes = 1; + DAS_InitNodes(); + } + + if ( !DSP_CheckDspAutoEnabled()) + { + // make sure room params are reinitialized each time autoroom is selected + + g_bdas_room_init = 0; + return; + } + + if ( !g_bdas_room_init ) + { + g_bdas_room_init = 1; + + DAS_InitAutoRoom( proom ); + } + + // get time + + double dtime = g_pSoundServices->GetHostTime(); + + // compare to previous time - don't check for new room until timer expires + // ie: wait at least DAS_AUTO_WAIT seconds between preset changes + + if ( fabs(dtime - proom->last_dsp_change) < DAS_AUTO_WAIT ) + return; + + // first, update room size parameters, see if room is ready to check - if room is updated, return true right away + + // 3 traces per frame while accumulating room size info + + for (int i = 0 ; i < 3; i++) + bRoom_ready = DAS_UpdateRoomSize( proom ); + + if (!bRoom_ready) + return; + + + if ( !g_bdas_create_new_node ) + { + // next, check all nodes for line of sight to player - if all checked, return true right away + + if ( !DAS_CheckNextNode( proom ) ) + { + // check all nodes first + + return; + } + + // find out if any previously stored nodes can see player, + // if so, get closest node's dsp preset + + dsp_preset = DAS_GetDspPreset( proom->bskyabove ); + + if (dsp_preset != -1) + { + // an existing node can see player - just set preset and return + + if (dsp_preset != dsp_room_GetInt()) + { + // changed preset, so update timestamp + + proom->last_dsp_change = g_pSoundServices->GetHostTime(); + + if (g_pdas_last_node) + DAS_DisplayRoomDEBUG( &(g_pdas_last_node->room), false, (float)dsp_preset ); + } + + DSP_SetDspAuto( dsp_preset ); + + goto check_new_room_exit; + } + } + + g_bdas_create_new_node = true; + + // no nodes can see player, need to try to create a new one + + // check for 'new' room around player + + if ( DAS_CheckNewRoom( proom ) ) + { + // new room found - update dsp_automatic + + dsp_preset = DAS_GetRoomDSP( proom, DAS_GetNextNodeIndex() ); + + DSP_SetDspAuto( dsp_preset ); + + // changed preset, so update timestamp + + proom->last_dsp_change = g_pSoundServices->GetHostTime(); + + // save room as new node + + DAS_StoreNode( proom, dsp_preset ); + + goto check_new_room_exit; + } + +check_new_room_exit: + + // reset new node creation flag - start checking for visible nodes again + + g_bdas_create_new_node = false; + + // reset room checking flag - start checking room around player again + + proom->broomready = false; + + // reset node checking flag - start checking nodes around player again + + DAS_ResetNodes(); + + return; +} + +// remap contents of volumes[] arrary if sound originates from player, or is music, and is 100% 'mono' +// ie: same volume in all channels + +void RemapPlayerOrMusicVols( channel_t *ch, int volumes[CCHANVOLUMES/2], bool fplayersound, bool fmusicsound, float mono ) +{ + VPROF_("RemapPlayerOrMusicVols", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + + if ( !fplayersound && !fmusicsound ) + return; // no remapping + + if ( ch->flags.bSpeaker ) + return; // don't remap speaker sounds rebroadcast on player + + // get total volume + + float vol_total = 0.0; + int k; + + for (k = 0; k < CCHANVOLUMES/2; k++) + vol_total += (float)volumes[k]; + + if ( !g_AudioDevice->IsSurround() ) + { + if (mono < 1.0) + return; + + // remap 2 chan non-spatialized versions of player and music sounds + // note: this is required to keep volumes same as 4 & 5 ch cases! + + float vol_dist_music[] = {1.0, 1.0}; // FL, FR music volumes + float vol_dist_player[] = {1.0, 1.0}; // FL, FR player volumes + float *pvol_dist; + + pvol_dist = (fplayersound ? vol_dist_player : vol_dist_music); + + for (k = 0; k < 2; k++) + volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); + + return; + } + + // surround sound configuration... + + if ( fplayersound ) // && (ch->bstereowav && ch->wavtype != CHAR_DIRECTIONAL && ch->wavtype != CHAR_DISTVARIANT) ) + { + // NOTE: player sounds also get n% overall volume boost. + + //float vol_dist5[] = {0.29, 0.29, 0.09, 0.09, 0.63}; // FL, FR, RL, RR, FC - 5 channel (mono source) volume distribution + //float vol_dist5st[] = {0.29, 0.29, 0.09, 0.09, 0.63}; // FL, FR, RL, RR, FC - 5 channel (stereo source) volume distribution + + float vol_dist5[] = {0.30, 0.30, 0.09, 0.09, 0.59}; // FL, FR, RL, RR, FC - 5 channel (mono source) volume distribution + float vol_dist5st[] = {0.30, 0.30, 0.09, 0.09, 0.59}; // FL, FR, RL, RR, FC - 5 channel (stereo source) volume distribution + + float vol_dist4[] = {0.50, 0.50, 0.15, 0.15, 0.00}; // FL, FR, RL, RR, 0 - 4 channel (mono source) volume distribution + float vol_dist4st[] = {0.50, 0.50, 0.15, 0.15, 0.00}; // FL, FR, RL, RR, 0 - 4 channel (stereo source)volume distribution + + float *pvol_dist; + + if ( ch->flags.bstereowav && (ch->wavtype == CHAR_OMNI || ch->wavtype == CHAR_SPATIALSTEREO || ch->wavtype == 0)) + { + pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5st : vol_dist4st); + } + else + { + pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5 : vol_dist4); + } + + for (k = 0; k < 5; k++) + volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); + + return; + } + + // Special case for music in surround mode + + if ( fmusicsound ) + { + float vol_dist5[] = {0.5, 0.5, 0.25, 0.25, 0.0}; // FL, FR, RL, RR, FC - 5 channel distribution + float vol_dist4[] = {0.5, 0.5, 0.25, 0.25, 0.0}; // FL, FR, RL, RR, 0 - 4 channel distribution + float *pvol_dist; + + pvol_dist = (g_AudioDevice->IsSurroundCenter() ? vol_dist5 : vol_dist4); + + for (k = 0; k < 5; k++) + volumes[k] = clamp((int)(vol_total * pvol_dist[k]), 0, 255); + + return; + } + + return; +} + +static int s_nSoundGuid = 0; + +void SND_ActivateChannel( channel_t *pChannel ) +{ + Q_memset( pChannel, 0, sizeof(*pChannel) ); + g_ActiveChannels.Add( pChannel ); + pChannel->guid = ++s_nSoundGuid; +} + +/* +================= +SND_Spatialize +================= +*/ +void SND_Spatialize(channel_t *ch) +{ + VPROF("SND_Spatialize"); + + vec_t dist; + Vector source_vec; + Vector source_vec_DL; + Vector source_vec_DR; + Vector source_doppler_left; + Vector source_doppler_right; + + bool fdopplerwav = false; + bool fplaydopplerwav = false; + bool fvalidentity; + float gain; + float scale = 1.0; + bool fplayersound = false; + bool fmusicsound = false; + float mono = 0.0; + bool bAttenuated = true; + + ch->dspface = 1.0; // default facing direction: always facing player + ch->dspmix = 0; // default mix 0% dsp_room fx + ch->distmix = 0; // default 100% left (near) wav + +#if !defined( _X360 ) + if ( ch->sfx && + ch->sfx->pSource && + ch->sfx->pSource->GetType() == CAudioSource::AUDIO_SOURCE_VOICE ) + { + Voice_Spatialize( ch ); + } +#endif + + if ( IsSoundSourceLocalPlayer( ch->soundsource ) && !toolframework->InToolMode() ) + { + // sounds coming from listener actually come from a short distance directly in front of listener + // in tool mode however, the view entity is meaningless, since we're viewing from arbitrary locations in space + fplayersound = true; + } + + // assume 'dry', playeverwhere sounds are 'music' or 'voiceover' + + if ( ch->flags.bdry && ch->dist_mult <= 0 ) + { + fmusicsound = true; + fplayersound = false; + } + + // update channel's position in case ent that made the sound is moving. + QAngle source_angles; + source_angles.Init(0.0, 0.0, 0.0); + Vector entOrigin = ch->origin; + + bool looping = false; + + CAudioSource *pSource = ch->sfx ? ch->sfx->pSource : NULL; + if ( pSource ) + { + looping = pSource->IsLooped(); + } + + SpatializationInfo_t si; + si.info.Set( + ch->soundsource, + ch->entchannel, + ch->sfx ? ch->sfx->getname() : "", + ch->origin, + ch->direction, + ch->master_vol, + DIST_MULT_TO_SNDLVL( ch->dist_mult ), + looping, + ch->pitch, + listener_origin, + ch->speakerentity ); + + si.type = SpatializationInfo_t::SI_INSPATIALIZATION; + si.pOrigin = &entOrigin; + si.pAngles = &source_angles; + si.pflRadius = NULL; + if ( ch->soundsource != 0 && ch->radius == 0 ) + { + si.pflRadius = &ch->radius; + } + + { + VPROF_("SoundServices->GetSoundSpatializtion", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + fvalidentity = g_pSoundServices->GetSoundSpatialization( ch->soundsource, si ); + } + + if ( ch->flags.bUpdatePositions ) + { + AngleVectors( source_angles, &ch->direction ); + ch->origin = entOrigin; + } + else + { + VectorAngles( ch->direction, source_angles ); + } + + if ( ch->userdata != 0 ) + { + g_pSoundServices->GetToolSpatialization( ch->userdata, ch->guid, si ); + if ( ch->flags.bUpdatePositions ) + { + AngleVectors( source_angles, &ch->direction ); + ch->origin = entOrigin; + } + } + +#if 0 + // !!!UNDONE - above code assumes the ENT hasn't been removed or respawned as another ent! + // !!!UNDONE - fix this by flagging some entities (ie: glass) as immobile. Don't spatialize them. + if ( !fvalidendity) + { + // Turn off the sound while the entity doesn't exist or is not in the PVS. + goto ClearAllVolumes; + } +#endif // 0 + + + fdopplerwav = ((ch->wavtype == CHAR_DOPPLER) && !fplayersound); + if ( fdopplerwav ) + { + VPROF_("SND_Spatialize doppler", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + Vector vnearpoint; // point of closest approach to listener, + // along sound source forward direction (doppler wavs) + + vnearpoint = ch->origin; // default nearest sound approach point + + // calculate point of closest approach for CHAR_DOPPLER wavs, replace source_vec + + fplaydopplerwav = SND_GetClosestPoint( ch, source_angles, vnearpoint ); + + // if doppler sound was 'shot' away from listener, don't play it + + if ( !fplaydopplerwav ) + goto ClearAllVolumes; + + // find location of doppler left & doppler right points + + SND_GetDopplerPoints( ch, source_angles, vnearpoint, source_doppler_left, source_doppler_right); + + // source_vec_DL is vector from listener to doppler left point + // source_vec_DR is vector from listener to doppler right point + + VectorSubtract(source_doppler_left, listener_origin, source_vec_DL ); + VectorSubtract(source_doppler_right, listener_origin, source_vec_DR ); + + // normalized vectors to left and right doppler locations + + dist = VectorNormalize( source_vec_DL ); + VectorNormalize( source_vec_DR ); + + // don't play doppler if out of range + // unless recording in the tool, since we may play back in range + if ( dist > DOPPLER_RANGE_MAX && !toolframework->IsToolRecording() ) + goto ClearAllVolumes; + } + else + { + // source_vec is vector from listener to sound source + + if ( fplayersound ) + { + // get 2d forward direction vector, ignoring pitch angle + Vector listener_forward2d; + + ConvertListenerVectorTo2D( &listener_forward2d, &listener_right ); + + // player sounds originate from 1' in front of player, 2d + + VectorMultiply(listener_forward2d, 12.0, source_vec ); + } + else + { + VectorSubtract(ch->origin, listener_origin, source_vec); + } + + // normalize source_vec and get distance from listener to source + + dist = VectorNormalize( source_vec ); + } + + // calculate dsp mix based on distance to listener & sound level (linear approximation) + + ch->dspmix = SND_GetDspMix( ch, dist ); + + // calculate sound source facing direction for CHAR_DIRECTIONAL wavs + + if ( !fplayersound ) + { + ch->dspface = SND_GetFacingDirection( ch, source_angles ); + + // calculate mixing parameter for CHAR_DISTVAR wavs + + ch->distmix = SND_GetDistanceMix( ch, dist ); + } + + // for sounds with a radius, spatialize left/right/front/rear evenly within the radius + + if ( ch->radius > 0 && dist < ch->radius && !fdopplerwav ) + { + float interval = ch->radius * 0.5; + mono = dist - interval; + if ( mono < 0.0 ) + mono = 0.0; + mono /= interval; + + mono = 1.0 - mono; + + // mono is 0.0 -> 1.0 from radius 100% to radius 50% + } + + // don't pan sounds with no attenuation + if ( ch->dist_mult <= 0 && !fdopplerwav ) + { + // sound is centered left/right/front/back + + mono = 1.0; + bAttenuated = false; + } + + if ( ch->wavtype == CHAR_OMNI ) + { + // omni directional sound sources are mono mix, all speakers + // ie: they only attenuate by distance, not by source direction. + + mono = 1.0; + bAttenuated = false; + } + + // calculate gain based on distance, atmospheric attenuation, interposed objects + // perform compression as gain approaches 1.0 + + gain = SND_GetGain( ch, fplayersound, fmusicsound, looping, dist, bAttenuated ); + + // map gain through global mixer by soundtype + + // gain *= SND_GetVolFromSoundtype( ch->soundtype ); + int last_mixgroupid; + + gain *= MXR_GetVolFromMixGroup( ch->mixgroups, &last_mixgroupid ); + + // if playing a word, get volume scale of word - scale gain + + scale = VOX_GetChanVol(ch); + + gain *= scale; + + // save spatialized volume and mixgroupid for display later + + ch->last_mixgroupid = last_mixgroupid; + + if ( fdopplerwav ) + { + VPROF_("SND_Spatialize doppler", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + // fill out channel volumes for both doppler sound source locations + int volumes[CCHANVOLUMES/2]; + + // left doppler location + + g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec_DL, gain, mono ); + + // load volumes into channel as crossfade targets + + ChannelSetVolTargets( ch, volumes, IFRONT_LEFT, CCHANVOLUMES/2 ); + + // right doppler location + + g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec_DR, gain, mono ); + + // load volumes into channel as crossfade targets + + ChannelSetVolTargets( ch, volumes, IFRONT_LEFTD, CCHANVOLUMES/2 ); + } + else + { + // fill out channel volumes for single sound source location + int volumes[CCHANVOLUMES/2]; + + g_AudioDevice->SpatializeChannel( volumes, ch->master_vol, source_vec, gain, mono ); + + // Special case for stereo sounds originating from player in surround mode + // and special case for musci: remap volumes directly to channels. + + RemapPlayerOrMusicVols( ch, volumes, fplayersound, fmusicsound, mono ); + + // load volumes into channel as crossfade volume targets + + ChannelSetVolTargets( ch, volumes, IFRONT_LEFT, CCHANVOLUMES/2 ); + } + + + // prevent left/right/front/rear/center volumes from changing too quickly & producing pops + + ChannelUpdateVolXfade( ch ); + + // end of first time spatializing sound + + if ( SND_IsInGame() || toolframework->InToolMode() ) + { + ch->flags.bfirstpass = false; + } + + // calculate total volume for display later + ch->last_vol = gain * (ch->master_vol/255.0); + + return; + +ClearAllVolumes: + + // Clear all volumes and return. + // This shuts the sound off permanently. + + ChannelClearVolumes( ch ); + + // end of first time spatializing sound + + ch->flags.bfirstpass = false; +} + +ConVar snd_defer_trace("snd_defer_trace","1"); +void SND_SpatializeFirstFrameNoTrace( channel_t *pChannel) +{ + if ( snd_defer_trace.GetBool() ) + { + // set up tracing state to be non-obstructed + pChannel->flags.bfirstpass = false; + pChannel->flags.bTraced = true; + pChannel->ob_gain = 1.0; + pChannel->ob_gain_inc = 1.0; + pChannel->ob_gain_target = 1.0; + // now spatialize without tracing + SND_Spatialize(pChannel); + // now reset tracing state to firstpass so the trace gets done on next spatialize + pChannel->ob_gain = 0.0; + pChannel->ob_gain_inc = 0.0; + pChannel->ob_gain_target = 0.0; + pChannel->flags.bfirstpass = true; + pChannel->flags.bTraced = false; + } + else + { + pChannel->ob_gain = 0.0; + pChannel->ob_gain_inc = 0.0; + pChannel->ob_gain_target = 0.0; + pChannel->flags.bfirstpass = true; + pChannel->flags.bTraced = false; + SND_Spatialize(pChannel); + } +} + + +// search through all channels for a channel that matches this +// soundsource, entchannel and sfx, and perform alteration on channel +// as indicated by 'flags' parameter. If shut down request and +// sfx contains a sentence name, shut off the sentence. +// returns TRUE if sound was altered, +// returns FALSE if sound was not found (sound is not playing) + +int S_AlterChannel( int soundsource, int entchannel, CSfxTable *sfx, int vol, int pitch, int flags ) +{ + THREAD_LOCK_SOUND(); + int ch_idx; + + const char *name = sfx->getname(); + if ( name && TestSoundChar( name, CHAR_SENTENCE ) ) + { + // This is a sentence name. + // For sentences: assume that the entity is only playing one sentence + // at a time, so we can just shut off + // any channel that has ch->isentence >= 0 and matches the + // soundsource. + + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + ch_idx = list.GetChannelIndex(i); + if (channels[ch_idx].soundsource == soundsource + && channels[ch_idx].entchannel == entchannel + && channels[ch_idx].sfx != NULL ) + { + + if (flags & SND_CHANGE_PITCH) + channels[ch_idx].basePitch = pitch; + + if (flags & SND_CHANGE_VOL) + channels[ch_idx].master_vol = vol; + + if (flags & SND_STOP) + { + S_FreeChannel(&channels[ch_idx]); + } + + return TRUE; + } + } + // channel not found + return FALSE; + + } + + // regular sound or streaming sound + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + + bool bSuccess = false; + + for ( int i = 0; i < list.Count(); i++ ) + { + ch_idx = list.GetChannelIndex(i); + if ( channels[ch_idx].soundsource == soundsource && + ( ( flags & SND_IGNORE_NAME ) || + ( channels[ch_idx].entchannel == entchannel && channels[ch_idx].sfx == sfx ) ) ) + { + if (flags & SND_CHANGE_PITCH) + channels[ch_idx].basePitch = pitch; + + if (flags & SND_CHANGE_VOL) + channels[ch_idx].master_vol = vol; + + if (flags & SND_STOP) + { + S_FreeChannel(&channels[ch_idx]); + } + + if ( ( flags & SND_IGNORE_NAME ) == 0 ) + return TRUE; + else + bSuccess = true; + } + } + + return ( bSuccess ) ? ( TRUE ) : ( FALSE ); +} + +// set channel flags during initialization based on +// source name + +void S_SetChannelWavtype( channel_t *target_chan, CSfxTable *pSfx ) +{ + // if 1st or 2nd character of name is CHAR_DRYMIX, sound should be mixed dry with no dsp (ie: music) + + if ( TestSoundChar(pSfx->getname(), CHAR_DRYMIX) ) + target_chan->flags.bdry = true; + else + target_chan->flags.bdry = false; + + if ( TestSoundChar(pSfx->getname(), CHAR_FAST_PITCH) ) + target_chan->flags.bfast_pitch = true; + else + target_chan->flags.bfast_pitch = false; + + // get sound spatialization encoding + + target_chan->wavtype = 0; + + if ( TestSoundChar( pSfx->getname(), CHAR_DOPPLER )) + target_chan->wavtype = CHAR_DOPPLER; + + if ( TestSoundChar( pSfx->getname(), CHAR_DIRECTIONAL )) + target_chan->wavtype = CHAR_DIRECTIONAL; + + if ( TestSoundChar( pSfx->getname(), CHAR_DISTVARIANT )) + target_chan->wavtype = CHAR_DISTVARIANT; + + if ( TestSoundChar( pSfx->getname(), CHAR_OMNI )) + target_chan->wavtype = CHAR_OMNI; + + if ( TestSoundChar( pSfx->getname(), CHAR_SPATIALSTEREO )) + target_chan->wavtype = CHAR_SPATIALSTEREO; +} + + +// Sets bstereowav flag in channel if source is true stere wav +// sets default wavtype for stereo wavs to CHAR_DISTVARIANT - +// ie: sound varies with distance (left is close, right is far) +// Must be called after S_SetChannelWavtype + +void S_SetChannelStereo( channel_t *target_chan, CAudioSource *pSource ) +{ + if ( !pSource ) + { + target_chan->flags.bstereowav = false; + return; + } + + // returns true only if source data is a stereo wav file. + // ie: mp3, voice, sentence are all excluded. + + target_chan->flags.bstereowav = pSource->IsStereoWav(); + + // Default stereo wavtype: + + // just player standard stereo wavs on player entity - no override. + + if ( IsSoundSourceLocalPlayer( target_chan->soundsource ) ) + return; + + // default wavtype for stereo wavs is OMNI - except for drymix or sounds with 0 attenuation + + if ( target_chan->flags.bstereowav && !target_chan->wavtype && !target_chan->flags.bdry && target_chan->dist_mult ) + // target_chan->wavtype = CHAR_DISTVARIANT; + target_chan->wavtype = CHAR_OMNI; +} + +// ======================================================================= +// Channel volume management routines: + +// channel volumes crossfade between values over time +// to prevent pops due to rapid spatialization changes +// ======================================================================= + +// return true if all volumes and target volumes for channel are less/equal to 'vol' + +bool BChannelLowVolume( channel_t *pch, int vol_min ) +{ + int max = -1; + int max_target = -1; + int vol; + int vol_target; + + for (int i = 0; i < CCHANVOLUMES; i++) + { + vol = (int)(pch->fvolume[i]); + vol_target = (int)(pch->fvolume_target[i]); + + if (vol > max) + max = vol; + + if (vol_target > max_target) + max_target = vol_target; + } + + return (max <= vol_min && max_target <= vol_min); +} + +// Get the loudest actual volume for a channel (not counting targets). +float ChannelLoudestCurVolume( const channel_t * RESTRICT pch ) +{ + float loudest = pch->fvolume[0]; + for (int i = 1; i < CCHANVOLUMES; i++) + { + loudest = fpmax(loudest, pch->fvolume[i]); + } + return loudest; +} + +// clear all volumes, targets, crossfade increments + +void ChannelClearVolumes( channel_t *pch ) +{ + for (int i = 0; i < CCHANVOLUMES; i++) + { + pch->fvolume[i] = 0.0; + pch->fvolume_target[i] = 0.0; + pch->fvolume_inc[i] = 0.0; + } +} + +// return current volume as integer + +int ChannelGetVol( channel_t *pch, int ivol ) +{ + Assert(ivol < CCHANVOLUMES); + return (int)(pch->fvolume[ivol]); +} + +// return maximum current output volume + +int ChannelGetMaxVol( channel_t *pch ) +{ + float max = 0.0; + + for (int i = 0; i < CCHANVOLUMES; i++) + { + if (pch->fvolume[i] > max) + max = pch->fvolume[i]; + } + + return (int)max; +} + +// set current volume (clears crossfading - instantaneous value change) + +void ChannelSetVol( channel_t *pch, int ivol, int vol ) +{ + Assert(ivol < CCHANVOLUMES); + + pch->fvolume[ivol] = (float)(clamp(vol, 0, 255)); + + pch->fvolume_target[ivol] = pch->fvolume[ivol]; + pch->fvolume_inc[ivol] = 0.0; +} + +// copy current channel volumes into target array, starting at ivol, copying cvol entries + +void ChannelCopyVolumes( channel_t *pch, int *pvolume_dest, int ivol_start, int cvol ) +{ + Assert (ivol_start < CCHANVOLUMES); + Assert (ivol_start + cvol <= CCHANVOLUMES); + + for (int i = 0; i < cvol; i++) + pvolume_dest[i] = (int)(pch->fvolume[i + ivol_start]); +} + +// volume has hit target, shut off crossfading increment + +inline void ChannelStopVolXfade( channel_t *pch, int ivol ) +{ + pch->fvolume[ivol] = pch->fvolume_target[ivol]; + pch->fvolume_inc[ivol] = 0.0; +} + +#define VOL_XFADE_TIME 0.070 // channel volume crossfade time in seconds + +#define VOL_INCR_MAX 20.0 // never change volume by more than +/-N units per frame + +// set volume target and volume increment (for crossfade) for channel & speaker + +void ChannelSetVolTarget( channel_t *pch, int ivol, int volume_target ) +{ + float frametime = g_pSoundServices->GetHostFrametime(); + float speed; + float vol_target = (float)(clamp(volume_target, 0, 255)); + float vol_current; + + Assert(ivol < CCHANVOLUMES); + + // set volume target + + pch->fvolume_target[ivol] = vol_target; + + // current volume + + vol_current = pch->fvolume[ivol]; + + // if first time spatializing, set target = volume with no crossfade + // if current & target volumes are close - don't bother crossfading + + if ( pch->flags.bfirstpass || (fabs(vol_target - vol_current) < 5.0)) + { + // set current volume = target, no increment + + ChannelStopVolXfade( pch, ivol); + return; + } + + // get crossfade increment 'speed' (volume change per frame) + + speed = ( frametime / VOL_XFADE_TIME ) * (vol_target - vol_current); + + // make sure we never increment by more than +/- VOL_INCR_MAX volume units per frame + + speed = clamp(speed, (float) -VOL_INCR_MAX, (float) VOL_INCR_MAX); + + pch->fvolume_inc[ivol] = speed; +} + +// set volume targets, using array pvolume as source volumes. +// set into channel volumes starting at ivol_offset index +// set cvol volumes + +void ChannelSetVolTargets( channel_t *pch, int *pvolumes, int ivol_offset, int cvol ) +{ + int volume_target; + + Assert(ivol_offset + cvol <= CCHANVOLUMES); + + for (int i = 0; i < cvol; i++) + { + volume_target = pvolumes[i]; + + ChannelSetVolTarget( pch, ivol_offset + i, volume_target ); + } +} + + +// Call once per frame, per channel: +// update all volume crossfades, from fvolume -> fvolume_target +// if current volume reaches target, set increment to 0 + +void ChannelUpdateVolXfade( channel_t *pch ) +{ + float fincr; + + for (int i = 0; i < CCHANVOLUMES; i++) + { + fincr = pch->fvolume_inc[i]; + + if (fincr != 0.0) + { + pch->fvolume[i] += fincr; + + // test for hit target + + if (fincr > 0.0) + { + if (pch->fvolume[i] >= pch->fvolume_target[i]) + ChannelStopVolXfade( pch, i ); + } + else + { + if (pch->fvolume[i] <= pch->fvolume_target[i]) + ChannelStopVolXfade( pch, i ); + } + } + } +} + +// ======================================================================= +// S_StartDynamicSound +// ======================================================================= +// Start a sound effect for the given entity on the given channel (ie; voice, weapon etc). +// Try to grab a channel out of the 8 dynamic spots available. +// Currently used for looping sounds, streaming sounds, sentences, and regular entity sounds. +// NOTE: volume is 0.0 - 1.0 and attenuation is 0.0 - 1.0 when passed in. +// Pitch changes playback pitch of wave by % above or below 100. Ignored if pitch == 100 + +// NOTE: it's not a good idea to play looping sounds through StartDynamicSound, because +// if the looping sound starts out of range, or is bumped from the buffer by another sound +// it will never be restarted. Use StartStaticSound (pass CHAN_STATIC to EMIT_SOUND or +// SV_StartSound. + +int S_StartDynamicSound( StartSoundParams_t& params ) +{ + Assert( params.staticsound == false ); + + channel_t *target_chan; + int vol; + + if ( !g_AudioDevice || !g_AudioDevice->IsActive()) + return 0; + + if (!params.pSfx) + return 0; + + // For debugging to see the actual name of the sound... + char sndname[ MAX_OSPATH ]; + Q_strncpy( sndname, params.pSfx->getname(), sizeof( sndname ) ); + + // Msg("Start sound %s\n", pSfx->getname() ); + + // override the entchannel to CHAN_STREAM if this is a + // non-voice stream sound. + if ( TestSoundChar(sndname, CHAR_STREAM ) && params.entchannel != CHAN_VOICE && params.entchannel != CHAN_VOICE2 ) + params.entchannel = CHAN_STREAM; + + vol = params.fvol*255; + + if (vol > 255) + { + DevMsg("S_StartDynamicSound: %s volume > 255", sndname ); + vol = 255; + } + + THREAD_LOCK_SOUND(); + + if ( params.flags & (SND_STOP|SND_CHANGE_VOL|SND_CHANGE_PITCH) ) + { + if ( S_AlterChannel( params.soundsource, params.entchannel, params.pSfx, vol, params.pitch, params.flags) ) + return 0; + if ( params.flags & SND_STOP ) + return 0; + // fall through - if we're not trying to stop the sound, + // and we didn't find it (it's not playing), go ahead and start it up + } + + if (params.pitch == 0) + { + DevMsg ("Warning: S_StartDynamicSound (%s) Ignored, called with pitch 0\n", sndname ); + return 0; + } + + // pick a channel to play on + target_chan = SND_PickDynamicChannel(params.soundsource, params.entchannel, params.origin, params.pSfx, params.delay, (params.flags & SND_DO_NOT_OVERWRITE_EXISTING_ON_CHANNEL) != 0 ); + if ( !target_chan ) + return 0; + + int channelIndex = (int)( target_chan - channels ); + g_AudioDevice->ChannelReset( params.soundsource, channelIndex, target_chan->dist_mult ); + +#ifdef DEBUG_CHANNELS + { + char szTmp[128]; + Q_snprintf(szTmp, sizeof( szTmp ), "Sound %s playing on Dynamic game channel %d\n", sndname, IWavstreamOfCh(target_chan)); + Plat_DebugString(szTmp); + } +#endif + + bool bIsSentence = TestSoundChar( sndname, CHAR_SENTENCE ); + + SND_ActivateChannel( target_chan ); + ChannelClearVolumes( target_chan ); + + target_chan->userdata = params.userdata; + target_chan->initialStreamPosition = params.initialStreamPosition; + + VectorCopy(params.origin, target_chan->origin); + VectorCopy(params.direction, target_chan->direction); + + // never update positions if source entity is 0 + target_chan->flags.bUpdatePositions = params.bUpdatePositions && (params.soundsource == 0 ? 0 : 1); + + // reference_dist / (reference_power_level / actual_power_level) + target_chan->flags.m_bCompatibilityAttenuation = SNDLEVEL_IS_COMPATIBILITY_MODE( params.soundlevel ); + if ( target_chan->flags.m_bCompatibilityAttenuation ) + { + // Translate soundlevel from its 'encoded' value to a real soundlevel that we can use in the sound system. + params.soundlevel = SNDLEVEL_FROM_COMPATIBILITY_MODE( params.soundlevel ); + } + + target_chan->dist_mult = SNDLVL_TO_DIST_MULT( params.soundlevel ); + + S_SetChannelWavtype( target_chan, params.pSfx ); + + target_chan->master_vol = vol; + target_chan->soundsource = params.soundsource; + target_chan->entchannel = params.entchannel; + target_chan->basePitch = params.pitch; + target_chan->flags.isSentence = false; + target_chan->radius = 0; + target_chan->sfx = params.pSfx; + target_chan->special_dsp = params.specialdsp; + target_chan->flags.fromserver = params.fromserver; + target_chan->flags.bSpeaker = (params.flags & SND_SPEAKER) ? 1 : 0; + target_chan->speakerentity = params.speakerentity; + + target_chan->flags.m_bShouldPause = (params.flags & SND_SHOULDPAUSE) ? 1 : 0; + + // initialize dsp room mixing params + target_chan->dsp_mix_min = -1; + target_chan->dsp_mix_max = -1; + + CAudioSource *pSource = NULL; + + if ( bIsSentence ) + { + // this is a sentence + // link all words and load the first word + + // NOTE: sentence names stored in the cache lookup are + // prepended with a '!'. Sentence names stored in the + // sentence file do not have a leading '!'. + VOX_LoadSound( target_chan, PSkipSoundChars( sndname ) ); + } + else + { + // regular or streamed sound fx + pSource = S_LoadSound( params.pSfx, target_chan ); + if ( pSource && !IsValidSampleRate( pSource->SampleRate() ) ) + { + Warning( "*** Invalid sample rate (%d) for sound '%s'.\n", pSource->SampleRate(), sndname ); + } + + if ( !pSource && !params.pSfx->m_bIsLateLoad ) + { + Warning( "Failed to load sound \"%s\", file probably missing from disk/repository\n", sndname ); + } + + } + + if (!target_chan->pMixer) + { + // couldn't load the sound's data, or sentence has 0 words (this is not an error) + S_FreeChannel( target_chan ); + return 0; + } + + int nSndShowStart = snd_showstart.GetInt(); + + // TODO: Support looping sounds through speakers. + // If the sound is from a speaker, and it's looping, ignore it. + if ( target_chan->flags.bSpeaker ) + { + if ( params.pSfx->pSource && params.pSfx->pSource->IsLooped() ) + { + if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) + { + DevMsg("DynamicSound : Speaker ignored looping sound: %s\n", sndname ); + } + + S_FreeChannel( target_chan ); + return 0; + } + } + + S_SetChannelStereo( target_chan, pSource ); + + if (nSndShowStart == 5) + { + snd_showstart.SetValue(6); // debug: show gain for next spatialize only + nSndShowStart = 6; + } + + // get sound type before we spatialize + MXR_GetMixGroupFromSoundsource( target_chan, params.soundsource, params.soundlevel ); + + // skip the trace on the first spatialization. This channel may be stolen + // by another sound played this frame. Defer the trace to the mix loop + SND_SpatializeFirstFrameNoTrace(target_chan); + + if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) + { + channel_t *pTargetChan = target_chan; + + DevMsg( "DynamicSound %s : src %d : channel %d : %d dB : vol %.2f : time %.3f\n", sndname, params.soundsource, params.entchannel, params.soundlevel, params.fvol, g_pSoundServices->GetHostTime() ); + if (nSndShowStart == 2 || nSndShowStart == 5) + DevMsg( "\t dspmix %1.2f : distmix %1.2f : dspface %1.2f : lvol %1.2f : cvol %1.2f : rvol %1.2f : rlvol %1.2f : rrvol %1.2f\n", + pTargetChan->dspmix, pTargetChan->distmix, pTargetChan->dspface, + pTargetChan->fvolume[IFRONT_LEFT], pTargetChan->fvolume[IFRONT_CENTER], pTargetChan->fvolume[IFRONT_RIGHT], pTargetChan->fvolume[IREAR_LEFT], pTargetChan->fvolume[IREAR_RIGHT] ); + if (nSndShowStart == 3) + DevMsg( "\t x: %4f y: %4f z: %4f\n", pTargetChan->origin.x, pTargetChan->origin.y, pTargetChan->origin.z ); + + if ( snd_visualize.GetInt() ) + { + CDebugOverlay::AddTextOverlay( pTargetChan->origin, 2.0f, sndname ); + } + } + + // If a client can't hear a sound when they FIRST receive the StartSound message, + // the client will never be able to hear that sound. This is so that out of + // range sounds don't fill the playback buffer. For streaming sounds, we bypass this optimization. + + if ( BChannelLowVolume( target_chan, 0 ) && !toolframework->IsToolRecording() ) + { + // Looping sounds don't use this optimization because they should stick around until they're killed. + // Also bypass for speech (GetSentence) + if ( !params.pSfx->pSource || (!params.pSfx->pSource->IsLooped() && !params.pSfx->pSource->GetSentence()) ) + { + // if this is long sound, play the whole thing. + if (!SND_IsLongWave( target_chan )) + { + // DevMsg("S_StartDynamicSound: spatialized to 0 vol & ignored %s", sndname); + S_FreeChannel( target_chan ); + return 0; // not audible at all + } + } + } + + // Init client entity mouth movement vars + target_chan->flags.m_bIgnorePhonemes = ( params.flags & SND_IGNORE_PHONEMES ) != 0; + SND_InitMouth(target_chan); + + if ( IsX360() && params.delay < 0 ) + { + params.delay = 0; + target_chan->flags.delayed_start = true; + } + + // Pre-startup delay. Compute # of samples over which to mix in zeros from data source before + // actually reading first set of samples + if ( params.delay != 0.0f ) + { + Assert( target_chan->sfx ); + Assert( target_chan->sfx->pSource ); + + // delay count is computed at the sampling rate of the source because the output rate will + // match the source rate when the sound is mixed + float rate = target_chan->sfx->pSource->SampleRate(); + int delaySamples = (int)( params.delay * rate ); + + if ( params.delay > 0 ) + { + target_chan->pMixer->SetStartupDelaySamples( delaySamples ); + target_chan->flags.delayed_start = true; + } + else + { + int skipSamples = -delaySamples; + int totalSamples = target_chan->sfx->pSource->SampleCount(); + if ( target_chan->sfx->pSource->IsLooped() ) + { + skipSamples = skipSamples % totalSamples; + } + if ( skipSamples >= totalSamples ) + { + S_FreeChannel( target_chan ); + return 0; + } + target_chan->pitch = target_chan->basePitch * 0.01f; + target_chan->pMixer->SkipSamples( target_chan, skipSamples, rate, 0 ); + target_chan->ob_gain_target = 1.0f; + target_chan->ob_gain = 1.0f; + target_chan->ob_gain_inc = 0.0; + target_chan->flags.bfirstpass = false; + target_chan->flags.delayed_start = true; + } + } + + g_pSoundServices->OnSoundStarted( target_chan->guid, params, sndname ); + return target_chan->guid; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *name - +// Output : CSfxTable +//----------------------------------------------------------------------------- +CSfxTable *S_DummySfx( const char *name ) +{ + dummySfx.setname( name ); + return &dummySfx; +} + +/* +================= +S_StartStaticSound +================= +Start playback of a sound, loaded into the static portion of the channel array. +Currently, this should be used for looping ambient sounds, looping sounds +that should not be interrupted until complete, non-creature sentences, +and one-shot ambient streaming sounds. Can also play 'regular' sounds one-shot, +in case designers want to trigger regular game sounds. +Pitch changes playback pitch of wave by % above or below 100. Ignored if pitch == 100 + + NOTE: volume is 0.0 - 1.0 and attenuation is 0.0 - 1.0 when passed in. +*/ + +int S_StartStaticSound( StartSoundParams_t& params ) +{ + Assert( params.staticsound == true ); + + channel_t *ch; + CAudioSource *pSource = NULL; + + if ( !g_AudioDevice->IsActive() ) + return 0; + + if ( !params.pSfx ) + return 0; + + // For debugging to see the actual name of the sound... + char sndname[ MAX_OSPATH ]; + Q_strncpy( sndname, params.pSfx->getname(), sizeof( sndname ) ); +// Msg("Start static sound %s\n", pSfx->getname() ); + + int vol = params.fvol * 255; + if ( vol > 255 ) + { + DevMsg( "S_StartStaticSound: %s volume > 255", sndname ); + vol = 255; + } + + int nSndShowStart = snd_showstart.GetInt(); + + if ((params.flags & SND_STOP) && nSndShowStart > 0) + DevMsg("S_StartStaticSound: %s Stopped.\n", sndname); + + if ((params.flags & SND_STOP) || (params.flags & SND_CHANGE_VOL) || (params.flags & SND_CHANGE_PITCH)) + { + if (S_AlterChannel(params.soundsource, params.entchannel, params.pSfx, vol, params.pitch, params.flags) || (params.flags & SND_STOP)) + return 0; + } + + if ( params.pitch == 0 ) + { + DevMsg( "Warning: S_StartStaticSound Ignored, called with pitch 0\n"); + return 0; + } + + // First, make sure the sound source entity is even in the PVS. + float flSoundRadius = 0.0f; + + bool looping = false; + + /* + CAudioSource *pSource = pSfx ? pSfx->pSource : NULL; + if ( pSource ) + { + looping = pSource->IsLooped(); + } + */ + + SpatializationInfo_t si; + si.info.Set( + params.soundsource, + params.entchannel, + params.pSfx ? sndname : "", + params.origin, + params.direction, + vol, + params.soundlevel, + looping, + params.pitch, + listener_origin, + params.speakerentity ); + + si.type = SpatializationInfo_t::SI_INCREATION; + + si.pOrigin = NULL; + si.pAngles = NULL; + si.pflRadius = &flSoundRadius; + + g_pSoundServices->GetSoundSpatialization( params.soundsource, si ); + + // pick a channel to play on from the static area + THREAD_LOCK_SOUND(); + + ch = SND_PickStaticChannel(params.soundsource, params.pSfx); // Autolooping sounds are always fixed origin(?) + if ( !ch ) + return 0; + + SND_ActivateChannel( ch ); + ChannelClearVolumes( ch ); + + ch->userdata = params.userdata; + ch->initialStreamPosition = params.initialStreamPosition; + + if ( ch->userdata != 0 ) + { + g_pSoundServices->GetToolSpatialization( ch->userdata, ch->guid, si ); + } + + int channelIndex = ch - channels; + g_AudioDevice->ChannelReset( params.soundsource, channelIndex, ch->dist_mult ); + +#ifdef DEBUG_CHANNELS + { + char szTmp[128]; + Q_snprintf(szTmp, sizeof( szTmp ), "Sound %s playing on Static game channel %d\n", sfxin->name, IWavstreamOfCh(ch)); + Plat_DebugString(szTmp); + } +#endif + + if ( TestSoundChar(sndname, CHAR_SENTENCE) ) + { + // this is a sentence. link words to play in sequence. + + // NOTE: sentence names stored in the cache lookup are + // prepended with a '!'. Sentence names stored in the + // sentence file do not have a leading '!'. + + // link all words and load the first word + VOX_LoadSound( ch, PSkipSoundChars(sndname) ); + } + else + { + // load regular or stream sound + pSource = S_LoadSound( params.pSfx, ch ); + if ( pSource && !IsValidSampleRate( pSource->SampleRate() ) ) + { + Warning( "*** Invalid sample rate (%d) for sound '%s'.\n", pSource->SampleRate(), sndname ); + } + + if ( !pSource && !params.pSfx->m_bIsLateLoad ) + { + Warning( "Failed to load sound \"%s\", file probably missing from disk/repository\n", sndname ); + } + + ch->sfx = params.pSfx; + ch->flags.isSentence = false; + } + + if ( !ch->pMixer ) + { + // couldn't load sounds' data, or sentence has 0 words (not an error) + S_FreeChannel( ch ); + return 0; + } + + VectorCopy (params.origin, ch->origin); + VectorCopy (params.direction, ch->direction); + + // never update positions if source entity is 0 + ch->flags.bUpdatePositions = params.bUpdatePositions && (params.soundsource == 0 ? 0 : 1); + + ch->master_vol = vol; + + ch->flags.m_bCompatibilityAttenuation = SNDLEVEL_IS_COMPATIBILITY_MODE( params.soundlevel ); + if ( ch->flags.m_bCompatibilityAttenuation ) + { + // Translate soundlevel from its 'encoded' value to a real soundlevel that we can use in the sound system. + params.soundlevel = SNDLEVEL_FROM_COMPATIBILITY_MODE( params.soundlevel ); + } + + ch->dist_mult = SNDLVL_TO_DIST_MULT( params.soundlevel ); + + S_SetChannelWavtype( ch, params.pSfx ); + + ch->basePitch = params.pitch; + ch->soundsource = params.soundsource; + ch->entchannel = params.entchannel; + ch->special_dsp = params.specialdsp; + ch->flags.fromserver = params.fromserver; + ch->flags.bSpeaker = (params.flags & SND_SPEAKER) ? 1 : 0; + ch->speakerentity = params.speakerentity; + + ch->flags.m_bShouldPause = (params.flags & SND_SHOULDPAUSE) ? 1 : 0; + + // TODO: Support looping sounds through speakers. + // If the sound is from a speaker, and it's looping, ignore it. + if ( ch->flags.bSpeaker ) + { + if ( params.pSfx->pSource && params.pSfx->pSource->IsLooped() ) + { + if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) + { + DevMsg("StaticSound : Speaker ignored looping sound: %s\n", sndname); + } + + S_FreeChannel( ch ); + return 0; + } + } + + // set the default radius + ch->radius = flSoundRadius; + + S_SetChannelStereo( ch, pSource ); + + // initialize dsp room mixing params + ch->dsp_mix_min = -1; + ch->dsp_mix_max = -1; + + if (nSndShowStart == 5) + { + snd_showstart.SetValue(6); // display gain once only + nSndShowStart = 6; + } + + // get sound type before we spatialize + + MXR_GetMixGroupFromSoundsource( ch, params.soundsource, params.soundlevel ); + + // skip the trace on the first spatialization. This channel may be stolen + // by another sound played this frame. Defer the trace to the mix loop + SND_SpatializeFirstFrameNoTrace(ch); + + // Init client entity mouth movement vars + ch->flags.m_bIgnorePhonemes = ( params.flags & SND_IGNORE_PHONEMES ) != 0; + SND_InitMouth( ch ); + + if ( IsX360() && params.delay < 0 ) + { + // X360TEMP: Can't support yet, but going to. + params.delay = 0; + } + + // Pre-startup delay. Compute # of samples over which to mix in zeros from data source before + // actually reading first set of samples + if ( params.delay != 0.0f ) + { + Assert( ch->sfx ); + Assert( ch->sfx->pSource ); + + float rate = ch->sfx->pSource->SampleRate(); + + int delaySamples = (int)( params.delay * rate * params.pitch * 0.01f ); + + ch->pMixer->SetStartupDelaySamples( delaySamples ); + + if ( params.delay > 0 ) + { + ch->pMixer->SetStartupDelaySamples( delaySamples ); + ch->flags.delayed_start = true; + } + else + { + int skipSamples = -delaySamples; + int totalSamples = ch->sfx->pSource->SampleCount(); + + if ( ch->sfx->pSource->IsLooped() ) + { + skipSamples = skipSamples % totalSamples; + } + + if ( skipSamples >= totalSamples ) + { + S_FreeChannel( ch ); + return 0; + } + + ch->pitch = ch->basePitch * 0.01f; + ch->pMixer->SkipSamples( ch, skipSamples, rate, 0 ); + ch->ob_gain_target = 1.0f; + ch->ob_gain = 1.0f; + ch->ob_gain_inc = 0.0f; + ch->flags.bfirstpass = false; + } + } + + if ( S_IsMusic( ch ) ) + { + // See if we have "music" of same name playing from "world" which means we save/restored this sound already. If so, + // kill the new version and update the soundsource + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = list.GetChannel(i); + // Don't mess with the channel we just created, of course + if ( ch == pChannel ) + continue; + if ( ch->sfx != pChannel->sfx ) + continue; + if ( pChannel->soundsource != SOUND_FROM_WORLD ) + continue; + if ( !S_IsMusic( pChannel ) ) + continue; + + DevMsg( 1, "Hooking duplicate restored song track %s\n", sndname ); + + // the new channel will have an updated soundsource and probably + // has an updated pitch or volume since we are receiving this sound message + // after the sound has started playing (usually a volume change) + // copy that data out of the source + pChannel->soundsource = ch->soundsource; + pChannel->master_vol = ch->master_vol; + pChannel->basePitch = ch->basePitch; + pChannel->pitch = ch->pitch; + S_FreeChannel( ch ); + + return 0; + } + } + + g_pSoundServices->OnSoundStarted( ch->guid, params, sndname ); + + if (nSndShowStart > 0 && nSndShowStart < 7 && nSndShowStart != 4) + { + DevMsg( "StaticSound %s : src %d : channel %d : %d dB : vol %.2f : radius %.0f : time %.3f\n", sndname, params.soundsource, params.entchannel, params.soundlevel, params.fvol, flSoundRadius, g_pSoundServices->GetHostTime() ); + if (nSndShowStart == 2 || nSndShowStart == 5) + DevMsg( "\t dspmix %1.2f : distmix %1.2f : dspface %1.2f : lvol %1.2f : cvol %1.2f : rvol %1.2f : rlvol %1.2f : rrvol %1.2f\n", + ch->dspmix, ch->distmix, ch->dspface, + ch->fvolume[IFRONT_LEFT], ch->fvolume[IFRONT_CENTER], ch->fvolume[IFRONT_RIGHT], ch->fvolume[IREAR_LEFT], ch->fvolume[IREAR_RIGHT] ); + if (nSndShowStart == 3) + DevMsg( "\t x: %4f y: %4f z: %4f\n", ch->origin.x, ch->origin.y, ch->origin.z ); + } + + return ch->guid; +} + +#ifdef STAGING_ONLY +static ConVar snd_filter( "snd_filter", "", FCVAR_CHEAT ); +#endif // STAGING_ONLY + +int S_StartSound( StartSoundParams_t& params ) +{ + + if( ! params.pSfx ) + { + return 0; + } + +#ifdef STAGING_ONLY + if ( snd_filter.GetString()[ 0 ] && !Q_stristr( params.pSfx->getname(), snd_filter.GetString() ) ) + { + return 0; + } +#endif // STAGING_ONLY + + if ( IsX360() && params.delay < 0 && !params.initialStreamPosition && params.pSfx ) + { + // calculate an initial stream position from the expected sample position + float rate = params.pSfx->pSource->SampleRate(); + int samplePosition = (int)( -params.delay * rate * params.pitch * 0.01f ); + params.initialStreamPosition = params.pSfx->pSource->SampleToStreamPosition( samplePosition ); + } + + if ( params.staticsound ) + { + VPROF_( "StartStaticSound", 0, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + return S_StartStaticSound( params ); + } + else + { + VPROF_( "StartDynamicSound", 0, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + return S_StartDynamicSound( params ); + } +} + +// Restart all the sounds on the specified channel +inline bool IsChannelLooped( int iChannel ) +{ + return (channels[iChannel].sfx && + channels[iChannel].sfx->pSource && + channels[iChannel].sfx->pSource->IsLooped() ); +} + +int S_GetCurrentStaticSounds( SoundInfo_t *pResult, int nSizeResult, int entchannel ) +{ + int nSpaceRemaining = nSizeResult; + for (int i = MAX_DYNAMIC_CHANNELS; i < total_channels && nSpaceRemaining; i++) + { + if ( channels[i].entchannel == entchannel && channels[i].sfx ) + { + pResult->Set( channels[i].soundsource, + channels[i].entchannel, + channels[i].sfx->getname(), + channels[i].origin, + channels[i].direction, + ( (float)channels[i].master_vol / 255.0 ), + DIST_MULT_TO_SNDLVL( channels[i].dist_mult ), + IsChannelLooped( i ), + channels[i].basePitch, + listener_origin, + channels[i].speakerentity ); + pResult++; + nSpaceRemaining--; + } + } + return (nSizeResult - nSpaceRemaining); +} + + +// Stop all sounds for entity on a channel. +void S_StopSound(int soundsource, int entchannel) +{ + THREAD_LOCK_SOUND(); + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = list.GetChannel(i); + if (pChannel->soundsource == soundsource + && pChannel->entchannel == entchannel) + { + S_FreeChannel( pChannel ); + } + } +} + +channel_t *S_FindChannelByGuid( int guid ) +{ + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = list.GetChannel(i); + if ( pChannel->guid == guid ) + { + return pChannel; + } + } + return NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : guid - +//----------------------------------------------------------------------------- +void S_StopSoundByGuid( int guid ) +{ + THREAD_LOCK_SOUND(); + channel_t *pChannel = S_FindChannelByGuid( guid ); + if ( pChannel ) + { + S_FreeChannel( pChannel ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : guid - +//----------------------------------------------------------------------------- +float S_SoundDurationByGuid( int guid ) +{ + channel_t *pChannel = S_FindChannelByGuid( guid ); + if ( !pChannel || !pChannel->sfx ) + return 0.0f; + + // NOTE: Looping sounds will return the length of a single loop + // Use S_IsLoopingSoundByGuid to see if they are looped + float flRate = pChannel->sfx->pSource->SampleRate() * pChannel->basePitch * 0.01f; + int nTotalSamples = pChannel->sfx->pSource->SampleCount(); + return (flRate != 0.0f) ? nTotalSamples / flRate : 0.0f; +} + + +//----------------------------------------------------------------------------- +// Is this sound a looping sound? +//----------------------------------------------------------------------------- +bool S_IsLoopingSoundByGuid( int guid ) +{ + channel_t *pChannel = S_FindChannelByGuid( guid ); + if ( !pChannel || !pChannel->sfx ) + return false; + + return( pChannel->sfx->pSource->IsLooped() ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Note that the guid is preincremented, so we can just return the current value as the "last sound" indicator +// Input : - +// Output : int +//----------------------------------------------------------------------------- +int S_GetGuidForLastSoundEmitted() +{ + return s_nSoundGuid; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : guid - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool S_IsSoundStillPlaying( int guid ) +{ + channel_t *pChannel = S_FindChannelByGuid( guid ); + return pChannel != NULL ? true : false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : guid - +// fvol - +//----------------------------------------------------------------------------- +void S_SetVolumeByGuid( int guid, float fvol ) +{ + channel_t *pChannel = S_FindChannelByGuid( guid ); + pChannel->master_vol = 255.0f * clamp( fvol, 0.0f, 1.0f ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : guid - +// Output : float +//----------------------------------------------------------------------------- +float S_GetElapsedTimeByGuid( int guid ) +{ + channel_t *pChannel = S_FindChannelByGuid( guid ); + if ( !pChannel ) + return 0.0f; + + CAudioMixer *mixer = pChannel->pMixer; + if ( !mixer ) + return 0.0f; + + float elapsed = mixer->GetSamplePosition() / ( mixer->GetSource()->SampleRate() * pChannel->pitch * 0.01f ); + return elapsed; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : sndlist - +//----------------------------------------------------------------------------- +void S_GetActiveSounds( CUtlVector< SndInfo_t >& sndlist ) +{ + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *ch = list.GetChannel(i); + + SndInfo_t info; + + info.m_nGuid = ch->guid; + info.m_filenameHandle = ch->sfx ? ch->sfx->GetFileNameHandle() : NULL; + info.m_nSoundSource = ch->soundsource; + info.m_nChannel = ch->entchannel; + // If a sound is being played through a speaker entity (e.g., on a monitor,), this is the + // entity upon which to show the lips moving, if the sound has sentence data + info.m_nSpeakerEntity = ch->speakerentity; + info.m_flVolume = (float)ch->master_vol / 255.0f; + info.m_flLastSpatializedVolume = ch->last_vol; + // Radius of this sound effect (spatialization is different within the radius) + info.m_flRadius = ch->radius; + info.m_nPitch = ch->basePitch; + info.m_pOrigin = &ch->origin; + info.m_pDirection = &ch->direction; + + // if true, assume sound source can move and update according to entity + info.m_bUpdatePositions = ch->flags.bUpdatePositions; + // true if playing linked sentence + info.m_bIsSentence = ch->flags.isSentence; + // if true, bypass all dsp processing for this sound (ie: music) + info.m_bDryMix = ch->flags.bdry; + // true if sound is playing through in-game speaker entity. + info.m_bSpeaker = ch->flags.bSpeaker; + // true if sound is using special DSP effect + info.m_bSpecialDSP = ( ch->special_dsp != 0 ); + // for snd_show, networked sounds get colored differently than local sounds + info.m_bFromServer = ch->flags.fromserver; + + sndlist.AddToTail( info ); + } +} + +void S_StopAllSounds( bool bClear ) +{ + THREAD_LOCK_SOUND(); + int i; + + if ( !g_AudioDevice ) + return; + + if ( !g_AudioDevice->IsActive() ) + return; + + total_channels = MAX_DYNAMIC_CHANNELS; // no statics + + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( i = 0; i < list.Count(); i++ ) + { + channel_t *pChannel = list.GetChannel(i); + if ( channels[i].sfx ) + { + DevMsg( 1, "%2d:Stopped sound %s\n", i, channels[i].sfx->getname() ); + } + S_FreeChannel( pChannel ); + } + + Q_memset( channels, 0, MAX_CHANNELS * sizeof(channel_t) ); + + if ( bClear ) + { + S_ClearBuffer(); + } + + // Clear any remaining soundfade + memset( &soundfade, 0, sizeof( soundfade ) ); + + g_AudioDevice->StopAllSounds(); + Assert( g_ActiveChannels.GetActiveCount() == 0 ); +} + +void S_StopAllSoundsC( void ) +{ + S_StopAllSounds( true ); +} + +void S_OnLoadScreen( bool value ) +{ + s_bOnLoadScreen = value; +} + +void S_ClearBuffer( void ) +{ + if ( !g_AudioDevice ) + return; + + g_AudioDevice->ClearBuffer(); + DSP_ClearState(); + MIX_ClearAllPaintBuffers( PAINTBUFFER_SIZE, true ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : percent - +// holdtime - +// intime - +// outtime - +//----------------------------------------------------------------------------- +void S_SoundFade( float percent, float holdtime, float intime, float outtime ) +{ + soundfade.starttime = g_pSoundServices->GetHostTime(); + + soundfade.initial_percent = percent; + soundfade.fadeouttime = outtime; + soundfade.holdtime = holdtime; + soundfade.fadeintime = intime; +} + +//----------------------------------------------------------------------------- +// Purpose: Modulates sound volume on the client. +//----------------------------------------------------------------------------- +void S_UpdateSoundFade(void) +{ + float totaltime; + float f; + // Determine current fade value. + + // Assume no fading remains + soundfade.percent = 0; + + totaltime = soundfade.fadeouttime + soundfade.fadeintime + soundfade.holdtime; + + float elapsed = g_pSoundServices->GetHostTime() - soundfade.starttime; + + // Clock wrapped or reset (BUG) or we've gone far enough + if ( elapsed < 0.0f || elapsed >= totaltime || totaltime <= 0.0f ) + { + return; + } + + // We are in the fade time, so determine amount of fade. + if ( soundfade.fadeouttime > 0.0f && ( elapsed < soundfade.fadeouttime ) ) + { + // Ramp up + f = elapsed / soundfade.fadeouttime; + } + // Inside the hold time + else if ( elapsed <= ( soundfade.fadeouttime + soundfade.holdtime ) ) + { + // Stay + f = 1.0f; + } + else + { + // Ramp down + f = ( elapsed - ( soundfade.fadeouttime + soundfade.holdtime ) ) / soundfade.fadeintime; + // backward interpolated... + f = 1.0f - f; + } + + // Spline it. + f = SimpleSpline( f ); + f = clamp( f, 0.0f, 1.0f ); + + soundfade.percent = soundfade.initial_percent * f; +} + + +//============================================================================= + +// Global Voice Ducker - enabled in vcd scripts, when characters deliver important dialog. Overrides all +// other mixer ducking, and ducks all other sounds except dialog. + +ConVar snd_ducktovolume( "snd_ducktovolume", "0.55", FCVAR_ARCHIVE ); +ConVar snd_duckerattacktime( "snd_duckerattacktime", "0.5", FCVAR_ARCHIVE ); +ConVar snd_duckerreleasetime( "snd_duckerreleasetime", "2.5", FCVAR_ARCHIVE ); +ConVar snd_duckerthreshold("snd_duckerthreshold", "0.15", FCVAR_ARCHIVE ); + +static void S_UpdateVoiceDuck( int voiceChannelCount, int voiceChannelMaxVolume, float frametime ) +{ + float volume_when_ducked = snd_ducktovolume.GetFloat(); + int volume_threshold = (int)(snd_duckerthreshold.GetFloat() * 255.0); + + float duckTarget = 1.0; + if ( voiceChannelCount > 0 ) + { + voiceChannelMaxVolume = clamp(voiceChannelMaxVolume, 0, 255); + + // duckTarget = RemapVal( voiceChannelMaxVolume, 0, 255, 1.0, volume_when_ducked ); + + // KB: Change: ducker now active if any character is speaking above threshold volume. + // KB: Active ducker drops all volumes to volumes * snd_duckvolume + + if ( voiceChannelMaxVolume > volume_threshold ) + duckTarget = volume_when_ducked; + } + float rate = ( duckTarget < g_DuckScale ) ? snd_duckerattacktime.GetFloat() : snd_duckerreleasetime.GetFloat(); + g_DuckScale = Approach( duckTarget, g_DuckScale, frametime * ((1-volume_when_ducked) / rate) ); +} + +// set 2d forward vector, given 3d right vector. +// NOTE: this should only be used for a listener forward +// vector from a listener right vector. It is not a general use routine. + +void ConvertListenerVectorTo2D( Vector *pvforward, Vector *pvright ) +{ + // get 2d forward direction vector, ignoring pitch angle + QAngle angles2d; + Vector source2d; + Vector listener_forward2d; + + source2d = *pvright; + source2d.z = 0.0; + + VectorNormalize(source2d); + + // convert right vector to euler angles (yaw & pitch) + + VectorAngles(source2d, angles2d); + + // get forward angle of listener + + angles2d[PITCH] = 0; + angles2d[YAW] += 90; // rotate 90 ccw + angles2d[ROLL] = 0; + + if (angles2d[YAW] >= 360) + angles2d[YAW] -= 360; + + AngleVectors(angles2d, &listener_forward2d); + + VectorNormalize(listener_forward2d); + + *pvforward = listener_forward2d; +} + +// If this is nonzero, we will only spatialize some of the static +// channels each frame. The round robin will spatialize 1 / (2 ^ x) +// of the spatial channels each frame. +ConVar snd_spatialize_roundrobin( "snd_spatialize_roundrobin", "0", FCVAR_ALLOWED_IN_COMPETITIVE, "Lowend optimization: if nonzero, spatialize only a fraction of sound channels each frame. 1/2^x of channels will be spatialized per frame." ); +/* +============ +S_Update + +Called once each time through the main loop +============ +*/ +void S_Update( const AudioState_t *pAudioState ) +{ + VPROF("S_Update"); + channel_t *ch; + channel_t *combine; + static unsigned int s_roundrobin = 0 ; ///< number of times this function is called. + ///< used instead of host_frame because that number + ///< isn't necessarily available here (sez Yahn). + + if ( !g_AudioDevice->IsActive() ) + return; + + g_SndMutex.Lock(); + + // Update any client side sound fade + S_UpdateSoundFade(); + + if ( pAudioState ) + { + VectorCopy( pAudioState->m_Origin, listener_origin ); + AngleVectors( pAudioState->m_Angles, &listener_forward, &listener_right, &listener_up ); + s_bIsListenerUnderwater = pAudioState->m_bIsUnderwater; + } + else + { + VectorCopy( vec3_origin, listener_origin ); + VectorCopy( vec3_origin, listener_forward ); + VectorCopy( vec3_origin, listener_right ); + VectorCopy( vec3_origin, listener_up ); + s_bIsListenerUnderwater = false; + } + + g_AudioDevice->UpdateListener( listener_origin, listener_forward, listener_right, listener_up ); + + combine = NULL; + + int voiceChannelCount = 0; + int voiceChannelMaxVolume = 0; + + // reset traceline counter for this frame + g_snd_trace_count = 0; + + // calculate distance to nearest walls, update dsp_spatial + // updates one wall only per frame (one trace per frame) + SND_SetSpatialDelays(); + + // updates dsp_room if automatic room detection enabled + DAS_CheckNewRoomDSP(); + + // update spatialization for static and dynamic sounds + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + + if (snd_spatialize_roundrobin.GetInt() == 0) + { + // spatialize each channel each time + for ( int i = 0; i < list.Count(); i++ ) + { + ch = list.GetChannel(i); + Assert(ch->sfx); + Assert(ch->activeIndex > 0); + + SND_Spatialize(ch); // respatialize channel + + if ( ch->sfx->pSource && ch->sfx->pSource->IsVoiceSource() ) + { + voiceChannelCount++; + voiceChannelMaxVolume = max(voiceChannelMaxVolume, ChannelGetMaxVol( ch) ); + } + } + } + else // lowend performance improvement: spatialize only some channels each frame. + { + unsigned int robinmask = (1 << snd_spatialize_roundrobin.GetInt()) - 1; + + // now do static channels + for ( int i = 0 ; i < list.Count() ; ++i ) + { + ch = list.GetChannel(i); + Assert(ch->sfx); + Assert(ch->activeIndex > 0); + + // need to check bfirstpass because sound tracing may have been deferred + if ( ch->flags.bfirstpass || (robinmask & s_roundrobin) == ( i & robinmask ) ) + { + SND_Spatialize(ch); // respatialize channel + } + + if ( ch->sfx->pSource && ch->sfx->pSource->IsVoiceSource() ) + { + voiceChannelCount++; + voiceChannelMaxVolume = max( voiceChannelMaxVolume, ChannelGetMaxVol( ch) ); + } + } + + ++s_roundrobin; + } + + + + SND_ChannelTraceReset(); + + // set new target for voice ducking + float frametime = g_pSoundServices->GetHostFrametime(); + S_UpdateVoiceDuck( voiceChannelCount, voiceChannelMaxVolume, frametime ); + + // update x360 music volume + g_DashboardMusicMixValue = Approach( g_DashboardMusicMixTarget, g_DashboardMusicMixValue, g_DashboardMusicFadeRate * frametime ); + + // + // debugging output + // + if (snd_show.GetInt()) + { + con_nprint_t np; + np.time_to_live = 2.0f; + np.fixed_width_font = true; + + int total = 0; + + CChannelList activeChannels; + g_ActiveChannels.GetActiveChannels( activeChannels ); + for ( int i = 0; i < activeChannels.Count(); i++ ) + { + channel_t *channel = activeChannels.GetChannel(i); + if ( !channel->sfx ) + continue; + + np.index = total + 2; + if ( channel->flags.fromserver ) + { + np.color[0] = 1.0; + np.color[1] = 0.8; + np.color[2] = 0.1; + } + else + { + np.color[0] = 0.1; + np.color[1] = 0.9; + np.color[2] = 1.0; + } + + unsigned int sampleCount = RemainingSamples( channel ); + float timeleft = (float)sampleCount / (float)channel->sfx->pSource->SampleRate(); + bool bLooping = channel->sfx->pSource->IsLooped(); + + if (snd_surround.GetInt() < 4) + { + Con_NXPrintf ( &np, "%02i l(%03d) r(%03d) vol(%03d) ent(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s", + total+ 1, + (int)channel->fvolume[IFRONT_LEFT], + (int)channel->fvolume[IFRONT_RIGHT], + channel->master_vol, + channel->soundsource, + (int)channel->origin[0], + (int)channel->origin[1], + (int)channel->origin[2], + timeleft, + bLooping, + channel->sfx->getname()); + } + else + { + Con_NXPrintf ( &np, "%02i l(%03d) c(%03d) r(%03d) rl(%03d) rr(%03d) vol(%03d) ent(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s", + total+ 1, + (int)channel->fvolume[IFRONT_LEFT], + (int)channel->fvolume[IFRONT_CENTER], + (int)channel->fvolume[IFRONT_RIGHT], + (int)channel->fvolume[IREAR_LEFT], + (int)channel->fvolume[IREAR_RIGHT], + channel->master_vol, + channel->soundsource, + (int)channel->origin[0], + (int)channel->origin[1], + (int)channel->origin[2], + timeleft, + bLooping, + channel->sfx->getname()); + } + + if ( snd_visualize.GetInt() ) + { + CDebugOverlay::AddTextOverlay( channel->origin, 0.05f, channel->sfx->getname() ); + } + + total++; + } + + while ( total <= 128 ) + { + Con_NPrintf( total + 2, "" ); + total++; + } + } + + g_SndMutex.Unlock(); + + if ( s_bOnLoadScreen ) + return; + + // not time to update yet? + double tNow = Plat_FloatTime(); + // this is the last time we ran a sound frame + g_LastSoundFrame = tNow; + // this is the last time we did mixing (extraupdate also advances this if it mixes) + g_LastMixTime = tNow; + // mix some sound + // try to stay at least one frame + mixahead ahead in the mix. + g_EstFrameTime = (g_EstFrameTime * 0.9f) + (g_pSoundServices->GetHostFrametime() * 0.1f); + S_Update_( g_EstFrameTime + snd_mixahead.GetFloat() ); +} + +CON_COMMAND( snd_dumpclientsounds, "Dump sounds to VXConsole" ) +{ + con_nprint_t np; + np.time_to_live = 2.0f; + np.fixed_width_font = true; + + int total = 0; + + CChannelList list; + g_ActiveChannels.GetActiveChannels( list ); + for ( int i = 0; i < list.Count(); i++ ) + { + channel_t *ch = list.GetChannel(i); + if ( !ch->sfx ) + continue; + + unsigned int sampleCount = RemainingSamples( ch ); + float timeleft = (float)sampleCount / (float)ch->sfx->pSource->SampleRate(); + bool bLooping = ch->sfx->pSource->IsLooped(); + const char *pszclassname = GetClientClassname(ch->soundsource); + + Msg( "%02i %s l(%03d) c(%03d) r(%03d) rl(%03d) rr(%03d) vol(%03d) pos(%6d %6d %6d) timeleft(%f) looped(%d) %50s chan:%d ent(%03d):%s\n", + total+ 1, + ch->flags.fromserver ? "SERVER" : "CLIENT", + (int)ch->fvolume[IFRONT_LEFT], + (int)ch->fvolume[IFRONT_CENTER], + (int)ch->fvolume[IFRONT_RIGHT], + (int)ch->fvolume[IREAR_LEFT], + (int)ch->fvolume[IREAR_RIGHT], + ch->master_vol, + (int)ch->origin[0], + (int)ch->origin[1], + (int)ch->origin[2], + timeleft, + bLooping, + ch->sfx->getname(), + ch->entchannel, + ch->soundsource, + pszclassname ? pszclassname : "NULL" ); + + total++; + } +} + +//----------------------------------------------------------------------------- +// Set g_soundtime to number of full samples that have been transfered out to hardware +// since start. +//----------------------------------------------------------------------------- +void GetSoundTime(void) +{ + int fullsamples; + int sampleOutCount; + + // size of output buffer in *full* 16 bit samples + // A 2 channel device has a *full* sample consisting of a 16 bit LR pair. + // A 1 channel device has a *full* sample consiting of a 16 bit single sample. + fullsamples = g_AudioDevice->DeviceSampleCount() / g_AudioDevice->DeviceChannels(); + + // NOTE: it is possible to miscount buffers if it has wrapped twice between + // calls to S_Update. However, since the output buffer size is > 1 second of sound, + // this should only occur for framerates lower than 1hz + + // sampleOutCount is counted in 16 bit *full* samples, of number of samples output to hardware + // for current output buffer + sampleOutCount = g_AudioDevice->GetOutputPosition(); + if ( sampleOutCount < s_oldsampleOutCount ) + { + // buffer wrapped + s_buffers++; + if ( g_paintedtime > 0x70000000 ) + { + // time to chop things off to avoid 32 bit limits + s_buffers = 0; + g_paintedtime = fullsamples; + S_StopAllSounds( true ); + } + } + + s_oldsampleOutCount = sampleOutCount; + + if ( cl_movieinfo.IsRecording() || IsReplayRendering() ) + { + // when recording a replay, we look at the record frame rate, not the engine frame rate + +#if defined( REPLAY_ENABLED ) + extern IClientReplayContext *g_pClientReplayContext; + if ( IsReplayRendering() ) + { + IReplayMovieRenderer *pMovieRenderer = (g_pClientReplayContext != NULL) ? g_pClientReplayContext->GetMovieRenderer() : NULL; + + if ( pMovieRenderer && pMovieRenderer->IsAudioSyncFrame() ) + { + float t = g_pSoundServices->GetHostTime(); + if ( s_lastsoundtime != t ) + { + float frameTime = pMovieRenderer->GetRecordingFrameDuration(); + float fSamples = frameTime * (float) g_AudioDevice->DeviceDmaSpeed() + g_ReplaySoundTimeFracAccumulator; + + float intPart = (float) floor( fSamples ); + g_ReplaySoundTimeFracAccumulator = fSamples - intPart; + + g_soundtime += (int) intPart; + s_lastsoundtime = t; + } + } + + } + else // cl_movieinfo.IsRecording() + // in movie, just mix one frame worth of sound +#endif + { + + float t = g_pSoundServices->GetHostTime(); + if ( s_lastsoundtime != t ) + { + g_soundtime += g_pSoundServices->GetHostFrametime() * g_AudioDevice->DeviceDmaSpeed(); + + s_lastsoundtime = t; + } + } + } + else + { + // g_soundtime indicates how many *full* samples have actually been + // played out to dma + g_soundtime = s_buffers*fullsamples + sampleOutCount; + } +} + +void S_ExtraUpdate( void ) +{ + if ( !g_AudioDevice || !g_pSoundServices ) + return; + + if ( !g_AudioDevice->IsActive() ) + return; + + if ( s_bOnLoadScreen ) + return; + + if ( snd_noextraupdate.GetInt() || cl_movieinfo.IsRecording() || IsReplayRendering() ) + return; // don't pollute timings + + // If listener position and orientation has not yet been updated (ie: no call to S_Update since level load) + // then don't mix. Important - mixing with listener at 'false' origin causes + // some sounds to incorrectly spatialize to 0 volume, killing them before they can play. + + if ((listener_origin == vec3_origin) && + (listener_forward == vec3_origin) && + (listener_right == vec3_origin) && + (listener_up == vec3_origin) ) + return; + + // Only mix if you have used up 90% of the mixahead buffer + double tNow = Plat_FloatTime(); + float delta = (tNow - g_LastMixTime); + // we know we were at least snd_mixahead seconds ahead of the output the last time we did mixing + // if we're not close to running out just exit to avoid small mix batches + if ( delta > 0 && delta < (snd_mixahead.GetFloat() * 0.9f) ) + return; + g_LastMixTime = tNow; + + g_pSoundServices->OnExtraUpdate(); + // Shouldn't have to do any work here if your framerate hasn't dropped + S_Update_( snd_mixahead.GetFloat() ); +} + +extern void DEBUG_StartSoundMeasure(int type, int samplecount ); +extern void DEBUG_StopSoundMeasure(int type, int samplecount ); + +void S_Update_Guts( float mixAheadTime ) +{ + VPROF( "S_Update_Guts" ); + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); + + DEBUG_StartSoundMeasure(4, 0); + + // Update our perception of audio time. + // 'g_soundtime' tells how many samples have + // been played out of the dma buffer since sound system startup. + // 'g_paintedtime' indicates how many samples we've actually mixed + // and sent to the dma buffer since sound system startup. + GetSoundTime(); + +// if ( g_soundtime > g_paintedtime ) +// { +// // if soundtime > paintedtime, then the dma buffer +// // has played out more sound than we've actually +// // mixed. We need to call S_Update_ more often. +// +// DevMsg ("S_Update_ : Underflow\n"); +// paintedtime = g_soundtime; +// } +// (kdb) above code doesn't handle underflow correctly +// should actually zero out the paintbuffer to advance to the new +// time. + + // mix ahead of current position + unsigned endtime = g_AudioDevice->PaintBegin( mixAheadTime, g_soundtime, g_paintedtime ); + + int samples = endtime - g_paintedtime; + samples = samples < 0 ? 0 : samples; + if ( samples ) + { + THREAD_LOCK_SOUND(); + + DEBUG_StartSoundMeasure( 2, samples ); + + MIX_PaintChannels( endtime, s_bIsListenerUnderwater ); + + MXR_DebugShowMixVolumes(); + + MXR_UpdateAllDuckerVolumes(); + + DEBUG_StopSoundMeasure( 2, 0 ); + } + + g_AudioDevice->PaintEnd(); + DEBUG_StopSoundMeasure( 4, samples ); +} + +#if !defined( _X360 ) +#define THREADED_MIX_TIME 33 +#else +#define THREADED_MIX_TIME XMA_POLL_RATE +#endif + +ConVar snd_ShowThreadFrameTime( "snd_ShowThreadFrameTime", "0" ); + +bool g_bMixThreadExit; +ThreadHandle_t g_hMixThread; +void S_Update_Thread() +{ + float frameTime = THREADED_MIX_TIME * 0.001f; + double lastFrameTime = Plat_FloatTime(); + + while ( !g_bMixThreadExit ) + { + // mixing (for 360) needs to be updated at a steady rate + // large update times causes the mixer to demand more audio data + // the 360 decoder has finite latency and cannot fulfill spike requests + float t0 = Plat_FloatTime(); + S_Update_Guts( frameTime + snd_mixahead.GetFloat() ); + int updateTime = ( Plat_FloatTime() - t0 ) * 1000.0f; + + // try to maintain a steadier rate by compensating for fluctuating mix times + int sleepTime = THREADED_MIX_TIME - updateTime; + if ( sleepTime > 0 ) + { + ThreadSleep( sleepTime ); + } + + // mimic a frametime needed for sound update + double t1 = Plat_FloatTime(); + frameTime = t1 - lastFrameTime; + lastFrameTime = t1; + + if ( snd_ShowThreadFrameTime.GetBool() ) + { + Msg( "S_Update_Thread: frameTime: %d ms\n", (int)( frameTime * 1000.0f ) ); + } + } +} + +void S_ShutdownMixThread() +{ + if ( g_hMixThread ) + { + g_bMixThreadExit = true; + ThreadJoin( g_hMixThread ); + ReleaseThreadHandle( g_hMixThread ); + g_hMixThread = NULL; + } +} + +void S_Update_( float mixAheadTime ) +{ + if ( !IsConsole() || !snd_mix_async.GetBool() ) + { + S_ShutdownMixThread(); + S_Update_Guts( mixAheadTime ); + } + else + { + if ( !g_hMixThread ) + { + g_bMixThreadExit = false; + g_hMixThread = ThreadExecuteSolo( "SndMix", S_Update_Thread ); + if ( IsX360() ) + { + ThreadSetAffinity( g_hMixThread, XBOX_PROCESSOR_5 ); + } + } + } +} + +//----------------------------------------------------------------------------- +// Threaded mixing enable. Purposely hiding enable/disable details. +//----------------------------------------------------------------------------- +void S_EnableThreadedMixing( bool bEnable ) +{ + if ( snd_mix_async.GetBool() != bEnable ) + { + snd_mix_async.SetValue( bEnable ); + } +} + +/* +=============================================================================== + +console functions + +=============================================================================== +*/ +extern void DSP_DEBUGSetParams(int ipreset, int iproc, float *pvalues, int cparams); +extern void DSP_DEBUGReloadPresetFile( void ); + +void S_DspParms( const CCommand &args ) +{ + if ( args.ArgC() == 1) + { + // if dsp_parms with no arguments, reload entire preset file + + DSP_DEBUGReloadPresetFile(); + + return; + } + + if ( args.ArgC() < 4 ) + { + Msg( "Usage: dsp_parms PRESET# PROC# param0 param1 ...up to param15 \n" ); + return; + } + + int cparam = min( args.ArgC() - 4, 16); + + float params[16]; + Q_memset( params, 0, sizeof(float) * 16 ); + + // get preset & proc + int idsp, iproc; + idsp = Q_atof( args[1] ); + iproc = Q_atof( args[2] ); + + // get params + for (int i = 0; i < cparam; i++) + { + params[i] = Q_atof( args[i+4] ); + } + + // set up params & switch preset + DSP_DEBUGSetParams(idsp, iproc, params, cparam); +} + +static ConCommand dsp_parm("dsp_reload", S_DspParms ); + +void S_Play( const char *pszName, bool flush = false ) +{ + int inCache; + char szName[256]; + CSfxTable *pSfx; + + Q_strncpy( szName, pszName, sizeof( szName ) ); + if ( !Q_strrchr( pszName, '.' ) ) + { + Q_strncat( szName, ".wav", sizeof( szName ), COPY_ALL_CHARACTERS ); + } + + pSfx = S_FindName( szName, &inCache ); + if ( inCache && flush ) + { + pSfx->pSource->CacheUnload(); + } + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = g_pSoundServices->GetViewEntity(); + params.entchannel = CHAN_REPLACE; + params.pSfx = pSfx; + params.origin = listener_origin; + params.fvol = 1.0f; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + + S_StartSound( params ); +} + +static void S_Play( const CCommand &args ) +{ + bool bFlush = !Q_stricmp( args[0], "playflush" ); + for ( int i = 1; i < args.ArgC(); ++i ) + { + S_Play( args[i], bFlush ); + } +} + +static void S_PlayVol( const CCommand &args ) +{ + static int hash=543; + float vol; + char name[256]; + CSfxTable *pSfx; + + for ( int i = 1; i<args.ArgC(); i += 2 ) + { + if ( !Q_strrchr( args[i], '.') ) + { + Q_strncpy( name, args[i], sizeof( name ) ); + Q_strncat( name, ".wav", sizeof( name ), COPY_ALL_CHARACTERS ); + } + else + { + Q_strncpy( name, args[i], sizeof( name ) ); + } + + pSfx = S_PrecacheSound( name ); + vol = Q_atof( args[i+1] ); + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = hash++; + params.entchannel = CHAN_AUTO; + params.pSfx = pSfx; + params.origin = listener_origin; + params.fvol = vol; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + + S_StartDynamicSound( params ); + } +} + +static void S_PlayDelay( const CCommand &args ) +{ + if ( args.ArgC() != 3 ) + { + Msg( "Usage: sndplaydelay delay_in_sec (negative to skip ahead) soundname\n" ); + return; + } + + char szName[256]; + CSfxTable *pSfx; + + float delay = Q_atof( args[ 1 ] ); + + Q_strncpy(szName, args[ 2 ], sizeof( szName ) ); + if ( !Q_strrchr( args[ 2 ], '.' ) ) + { + Q_strncat( szName, ".wav", sizeof( szName ), COPY_ALL_CHARACTERS ); + } + + pSfx = S_FindName( szName, NULL ); + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = g_pSoundServices->GetViewEntity(); + params.entchannel = CHAN_REPLACE; + params.pSfx = pSfx; + params.origin = listener_origin; + params.fvol = 1.0f; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + params.delay = delay; + + S_StartSound( params ); + +} +static ConCommand sndplaydelay( "sndplaydelay", S_PlayDelay, "Usage: sndplaydelay delay_in_sec (negative to skip ahead) soundname", FCVAR_SERVER_CAN_EXECUTE ); + +static bool SortByNameLessFunc( const int &lhs, const int &rhs ) +{ + CSfxTable *pSfx1 = s_Sounds[lhs].pSfx; + CSfxTable *pSfx2 = s_Sounds[rhs].pSfx; + + return CaselessStringLessThan( pSfx1->getname(), pSfx2->getname() ); +} + +void S_SoundList(void) +{ + CSfxTable *sfx; + CAudioSource *pSource; + int size, total; + + total = 0; + for ( int i = s_Sounds.FirstInorder(); i != s_Sounds.InvalidIndex(); i = s_Sounds.NextInorder( i ) ) + { + sfx = s_Sounds[i].pSfx; + + pSource = sfx->pSource; + if ( !pSource || !pSource->IsCached() ) + continue; + + size = pSource->SampleSize() * pSource->SampleCount(); + total += size; + + if ( pSource->IsLooped() ) + Msg ("L"); + else + Msg (" "); + Msg("(%2db) %6i : %s\n", pSource->SampleSize(), size, sfx->getname()); + } + Msg( "Total resident: %i\n", total ); +} + +#if defined( _X360 ) +CON_COMMAND( vx_soundlist, "Dump sounds to VXConsole" ) +{ + CSfxTable *sfx; + CAudioSource *pSource; + int dataSize; + char *pFormatStr; + int sampleRate; + int sampleBits; + int streamed; + int looped; + int channels; + int numSamples; + + int numSounds = s_Sounds.Count(); + xSoundList_t* pSoundList = new xSoundList_t[numSounds]; + + int i = 0; + for ( int iSrcSound=s_Sounds.FirstInorder(); iSrcSound != s_Sounds.InvalidIndex(); iSrcSound = s_Sounds.NextInorder( iSrcSound ) ) + { + dataSize = -1; + sampleRate = -1; + sampleBits = -1; + pFormatStr = "???"; + streamed = -1; + looped = -1; + channels = -1; + numSamples = -1; + + sfx = s_Sounds[iSrcSound].pSfx; + pSource = sfx->pSource; + if ( pSource && pSource->IsCached() ) + { + numSamples = pSource->SampleCount(); + dataSize = pSource->DataSize(); + sampleRate = pSource->SampleRate(); + streamed = pSource->IsStreaming(); + looped = pSource->IsLooped(); + channels = pSource->IsStereoWav() ? 2 : 1; + + if ( pSource->Format() == WAVE_FORMAT_ADPCM ) + { + pFormatStr = "ADPCM"; + sampleBits = 16; + } + else if ( pSource->Format() == WAVE_FORMAT_PCM ) + { + pFormatStr = "PCM"; + sampleBits = (pSource->SampleSize() * 8)/channels; + } + else if ( pSource->Format() == WAVE_FORMAT_XMA ) + { + pFormatStr = "XMA"; + sampleBits = 16; + } + } + + V_strncpy( pSoundList[i].name, sfx->getname(), sizeof( pSoundList[i].name ) ); + V_strncpy( pSoundList[i].formatName, pFormatStr, sizeof( pSoundList[i].formatName ) ); + pSoundList[i].rate = sampleRate; + pSoundList[i].bits = sampleBits; + pSoundList[i].channels = channels; + pSoundList[i].looped = looped; + pSoundList[i].dataSize = dataSize; + pSoundList[i].numSamples = numSamples; + pSoundList[i].streamed = streamed; + ++i; + } + + XBX_rSoundList( numSounds, pSoundList ); + delete [] pSoundList; +} +#endif + +extern unsigned g_snd_time_debug; +extern unsigned g_snd_call_time_debug; +extern unsigned g_snd_count_debug; +extern unsigned g_snd_samplecount; +extern unsigned g_snd_frametime; +extern unsigned g_snd_frametime_total; +extern int g_snd_profile_type; + +// start measuring sound perf, 100 reps +// type 1 - dsp, 2 - mix, 3 - load sound, 4 - all sound +// set type via ConVar snd_profile + +void DEBUG_StartSoundMeasure(int type, int samplecount ) +{ + if (type != g_snd_profile_type) + return; + + if (samplecount) + g_snd_samplecount += samplecount; + + g_snd_call_time_debug = Plat_MSTime(); +} + +// show sound measurement after 25 reps - show as % of total frame +// type 1 - dsp, 2 - mix, 3 - load sound, 4 - all sound + +// BUGBUG: snd_profile 4 reports a lower average because it's average cost +// PER CALL and most calls (via SoundExtraUpdate()) don't do any work and +// bring the average down. If you want an average PER FRAME instead, it's generally higher. +void DEBUG_StopSoundMeasure(int type, int samplecount ) +{ + if (type != g_snd_profile_type) + return; + + if (samplecount) + g_snd_samplecount += samplecount; + + // add total time since last frame + + g_snd_frametime_total += Plat_MSTime() - g_snd_frametime; + + // performance timing + + g_snd_time_debug += Plat_MSTime() - g_snd_call_time_debug; + + if (++g_snd_count_debug >= 100) + { + switch (g_snd_profile_type) + { + case 1: + Msg("dsp: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); + Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); + break; + case 2: + Msg("mix+dsp:(%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); + Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); + break; + case 3: + //if ( (((float)g_snd_time_debug) / 100.0) < 0.01 ) + // break; + Msg("snd load: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); + Msg("(%2.2f) pct of frame \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total)); + break; + case 4: + Msg("sound: (%2.2f) millisec ", ((float)g_snd_time_debug) / 100.0); + Msg("(%2.2f) pct of frame (%d samples) \n", 100.0 * ((float)g_snd_time_debug) / ((float)g_snd_frametime_total), g_snd_samplecount); + break; + } + + g_snd_count_debug = 0; + g_snd_time_debug = 0; + g_snd_samplecount = 0; + g_snd_frametime_total = 0; + } + + g_snd_frametime = Plat_MSTime(); +} + +// speak a sentence from console; works by passing in "!sentencename" +// or "sentence" + +extern ConVar dsp_room; + +static void S_Say( const CCommand &args ) +{ + CSfxTable *pSfx; + + if ( !g_AudioDevice->IsActive() ) + return; + + char sound[256]; + Q_strncpy( sound, args[1], sizeof( sound ) ); + + // DEBUG - test performance of dsp code + if ( !Q_stricmp( sound, "dsp" ) ) + { + unsigned time; + int i; + int count = 10000; + int idsp; + + for (i = 0; i < PAINTBUFFER_SIZE; i++) + { + g_paintbuffer[i].left = RandomInt(0,2999); + g_paintbuffer[i].right = RandomInt(0,2999); + } + + Msg ("Start profiling 10,000 calls to DSP\n"); + + idsp = dsp_room.GetInt(); + + // get system time + + time = Plat_MSTime(); + + for (i = 0; i < count; i++) + { + // SX_RoomFX(PAINTBUFFER_SIZE, TRUE, TRUE); + + DSP_Process(idsp, g_paintbuffer, NULL, NULL, PAINTBUFFER_SIZE); + + } + // display system time delta + Msg("%d milliseconds \n", Plat_MSTime() - time); + return; + } + + if ( !Q_stricmp(sound, "paint") ) + { + unsigned time; + int count = 10000; + static int hash=543; + int psav = g_paintedtime; + + Msg ("Start profiling MIX_PaintChannels\n"); + + pSfx = S_PrecacheSound("ambience/labdrone1.wav"); + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = hash++; + params.entchannel = CHAN_AUTO; + params.pSfx = pSfx; + params.origin = listener_origin; + params.fvol = 1.0f; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + + S_StartDynamicSound( params ); + + // get system time + time = Plat_MSTime(); + + // paint a boatload of sound + + MIX_PaintChannels( g_paintedtime + 512*count, s_bIsListenerUnderwater ); + + // display system time delta + Msg("%d milliseconds \n", Plat_MSTime() - time); + g_paintedtime = psav; + return; + } + + // DEBUG + if ( !TestSoundChar( sound, CHAR_SENTENCE ) ) + { + // build a fake sentence name, then play the sentence text + + Q_strncpy(sound, "xxtestxx ", sizeof( sound ) ); + Q_strncat(sound, args[1], sizeof( sound ), COPY_ALL_CHARACTERS ); + + int addIndex = g_Sentences.AddToTail(); + sentence_t *pSentence = &g_Sentences[addIndex]; + pSentence->pName = sound; + pSentence->length = 0; + + // insert null terminator after sentence name + sound[8] = 0; + + pSfx = S_PrecacheSound ("!xxtestxx"); + if (!pSfx) + { + Msg ("S_Say: can't cache %s\n", sound); + return; + } + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = g_pSoundServices->GetViewEntity(); + params.entchannel = CHAN_REPLACE; + params.pSfx = pSfx; + params.origin = vec3_origin; + params.fvol = 1.0f; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + + S_StartDynamicSound ( params ); + + // remove last + g_Sentences.Remove( g_Sentences.Size() - 1 ); + } + else + { + pSfx = S_FindName(sound, NULL); + if (!pSfx) + { + Msg ("S_Say: can't find sentence name %s\n", sound); + return; + } + + StartSoundParams_t params; + params.staticsound = false; + params.soundsource = g_pSoundServices->GetViewEntity(); + params.entchannel = CHAN_REPLACE; + params.pSfx = pSfx; + params.origin = vec3_origin; + params.fvol = 1.0f; + params.soundlevel = SNDLVL_NONE; + params.flags = 0; + params.pitch = PITCH_NORM; + + S_StartDynamicSound( params ); + } +} + + +//------------------------------------------------------------------------------ +// +// Sound Mixers +// +// Sound mixers are referenced by name from Soundscapes, and are used to provide +// custom volume control over various sound categories, called 'mix groups' +// +// see scripts/soundmixers.txt for data format +//------------------------------------------------------------------------------ + +#define CMXRGROUPMAX 64 // up to n mixgroups +#define CMXRGROUPRULESMAX (CMXRGROUPMAX + 16) // max number of group rules +#define CMXRSOUNDMIXERSMAX 32 // up to n sound mixers per project + +// mix groups - these equivalent to submixes on an audio mixer + +// list of rules for determining sound membership in mix groups. +// All conditions which are not null are ANDed together +#define CMXRCLASSMAX 16 +#define CMXRNAMEMAX 32 + +struct classlistelem_t +{ + char szclassname[CMXRNAMEMAX]; // name of entities' class, such as CAI_BaseNPC or CHL2_Player +}; + + +struct grouprule_t +{ + char szmixgroup[CMXRNAMEMAX]; // mix group name + int mixgroupid; // mix group unique id + char szdir[CMXRNAMEMAX]; // substring to search for in ch->sfx + int classId; // index of classname + int chantype; // channel type (CHAN_WEAPON, etc) + int soundlevel_min; // min soundlevel + int soundlevel_max; // max soundlevel + + int priority; // 0..100 higher priority sound groups duck all lower pri groups if enabled + int is_ducked; // if 1, sound group is ducked by all higher priority 'causes_duck" sounds + int causes_ducking; // if 1, sound group ducks other 'is_ducked' sounds of lower priority + float duck_target_pct; // if sound group is ducked, target percent of original volume + + float total_vol; // total volume of all sounds in this group, if group can cause ducking + float ducker_threshold; // ducking is caused by this group if total_vol > ducker_threshold + // and causes_ducking is enabled. + float duck_target_vol; // target volume while ducking + float duck_ramp_val; // current value of ramp - moves towards duck_target_vol +}; + +// sound mixer + +struct soundmixer_t +{ + char szsoundmixer[CMXRNAMEMAX]; // name of this soundmixer + float mapMixgroupidToValue[CMXRGROUPMAX]; // sparse array of mix group values for this soundmixer +}; + +int g_mapMixgroupidToGrouprulesid[CMXRGROUPMAX]; // map mixgroupid (one per unique group name) + // back to 1st entry of this name in g_grouprules + +// sound mixer globals + +classlistelem_t g_groupclasslist[CMXRCLASSMAX]; +soundmixer_t g_soundmixers[CMXRSOUNDMIXERSMAX]; // all sound mixers +grouprule_t g_grouprules[CMXRGROUPRULESMAX]; // all rules for determining mix group membership + + +// set current soundmixer index g_isoundmixer, search for match in soundmixers +// Only change current soundmixer if new name is different from current name. + +int g_isoundmixer = -1; // index of current sound mixer +char g_szsoundmixer_cur[64]; // current soundmixer name + +ConVar snd_soundmixer("snd_soundmixer", "Default_Mix"); // current soundmixer name + + +void MXR_SetCurrentSoundMixer( const char *szsoundmixer ) +{ + // if soundmixer name is not different from current name, return + + if ( !Q_stricmp(szsoundmixer, g_szsoundmixer_cur) ) + { + return; + } + + for (int i = 0; i < g_csoundmixers; i++) + { + if ( !Q_stricmp(g_soundmixers[i].szsoundmixer, szsoundmixer) ) + { + g_isoundmixer = i; + + // save new current sound mixer name + V_strcpy_safe(g_szsoundmixer_cur, szsoundmixer); + + return; + } + } +} + +ConVar snd_showclassname("snd_showclassname", "0"); // if 1, show classname of ent making sound + // if 2, show all mixgroup matches + // if 3, show all mixgroup matches with current soundmixer for ent +// get the client class name if an entity was specified +const char *GetClientClassname( SoundSource soundsource ) +{ + IClientEntity *pClientEntity = NULL; + if ( entitylist ) + { + pClientEntity = entitylist->GetClientEntity( soundsource ); + if ( pClientEntity ) + { + ClientClass *pClientClass = pClientEntity->GetClientClass(); + // check npc sounds + if ( pClientClass ) + { + return pClientClass->GetName(); + } + } + } + + return NULL; +} + +// builds a cached list of rules that match the directory name on the sound +int MXR_GetMixGroupListFromDirName( const char *pDirname, byte *pList, int listMax ) +{ + // if we call this before the groups are parsed we'll get bad data + Assert(g_cgrouprules>0); + int count = 0; + for ( int i = 0; i < listMax; i++ ) + { + pList[i] = 255; + } + + for ( int i = 0; i < g_cgrouprules; i++ ) + { + grouprule_t *prule = &g_grouprules[i]; + if ( prule->szdir[ 0 ] && Q_stristr( pDirname, prule->szdir ) ) + { + pList[count] = i; + count++; + if ( count >= listMax ) + return count; + } + } + return count; +} + + +// determine which mixgroups sound is in, and save those mixgroupids in sound. +// use current soundmixer indicated with g_isoundmixer, and contents of g_rgpgrouprules. +// Algorithm: +// 1. all conditions in a row are AND conditions, +// 2. all rows sharing the same groupname are OR conditions. +// so - if a sound matches all conditions of a row, it is given that row's mixgroup id +// if a sound doesn't match all conditions of a row, the next row is checked. + +// returns 0, default mixgroup if no match +void MXR_GetMixGroupFromSoundsource( channel_t *pchan, SoundSource soundsource, soundlevel_t soundlevel) +{ + int i; + grouprule_t *prule; + bool fmatch; + bool classMatch[CMXRCLASSMAX]; + + // init all mixgroups for channel + for ( i = 0; i < 8; i++ ) + { + pchan->mixgroups[i] = -1; + } + + char sndname[MAX_OSPATH]; + Q_strncpy( sndname, pchan->sfx->getname(), sizeof( sndname ) ); + // Use forward slashes here + Q_FixSlashes( sndname, '/' ); + const char *pszclassname = GetClientClassname(soundsource); + + for ( i = 0; i < g_cgroupclass; i++ ) + { + classMatch[i] = false; + if ( pszclassname && Q_stristr(pszclassname, g_groupclasslist[i].szclassname ) ) + { + classMatch[i] = true; + } + } + + if ( snd_showclassname.GetInt() == 1) + { + // utility: show classname of ent making sound + + if (pszclassname) + { + DevMsg("(%s:%s) \n", pszclassname, sndname); + } + } + + // check all group rules for a match, save + // up to 8 matches in channel mixgroup. + + int cmixgroups = 0; + if (!pchan->sfx->m_bMixGroupsCached) + { + pchan->sfx->OnNameChanged( pchan->sfx->getname() ); + } + + // since this is a sorted list (in group rule order) we only need to test against the next matching rule + // this avoids a search inside the loop + int currentDirRuleIndex = 0; + int currentDirRule = pchan->sfx->m_mixGroupList[0]; + + for (i = 0; i < g_cgrouprules; i++) + { + prule = &g_grouprules[i]; + fmatch = true; + + // check directory or name substring +#if _DEBUG + // check dir table is correct in CSfxTable cache + if ( prule->szdir[ 0 ] && Q_stristr( sndname, prule->szdir ) ) + { + Assert(currentDirRule == i); + } + else + { + Assert(currentDirRule != i); + } + if ( prule->classId >= 0 ) + { + // rule has a valid class id and table is correct + Assert(prule->classId < g_cgroupclass); + if ( pszclassname && Q_stristr(pszclassname, g_groupclasslist[prule->classId].szclassname) ) + { + Assert(classMatch[prule->classId] == true); + } + else + { + Assert(classMatch[prule->classId] == false); + } + } +#endif + // this is the next matching dir for this sound, no need to search + // becuse the list is sorted and we visit all elements + if ( currentDirRule == i ) + { + Assert(prule->szdir[0]); + currentDirRuleIndex++; + currentDirRule = 255; + if ( currentDirRuleIndex < pchan->sfx->m_mixGroupCount ) + { + currentDirRule = pchan->sfx->m_mixGroupList[currentDirRuleIndex]; + } + } + else if ( prule->szdir[ 0 ] ) + { + fmatch = false; // substring doesn't match, keep looking + } + + // check class name + + if ( fmatch && prule->classId >= 0 ) + { + fmatch = classMatch[prule->classId]; + } + + // check channel type + + if ( fmatch && prule->chantype >= 0) + { + if ( pchan->entchannel != prule->chantype ) + fmatch = false; // channel type doesn't match, keep looking + } + + // check sndlvlmin/max + + if ( fmatch && prule->soundlevel_min >= 0) + { + if ( soundlevel < prule->soundlevel_min ) + fmatch = false; // soundlevel is less than min, keep looking + } + + if ( fmatch && prule->soundlevel_max >= 0) + { + if ( soundlevel > prule->soundlevel_max ) + fmatch = false; // soundlevel is greater than max, keep looking + } + + if ( fmatch ) + { + pchan->mixgroups[cmixgroups] = prule->mixgroupid; + cmixgroups++; + if (cmixgroups >= 8) + return; // too many matches, stop looking + } + + if (fmatch && snd_showclassname.GetInt() >= 2) + { + // show all mixgroups for this sound + if (cmixgroups == 1) + { + DevMsg("\n%s:%s: ", g_szsoundmixer_cur, sndname); + } + if (prule->szmixgroup[0]) + { + // int rgmixgroupid[8]; + // for (int i = 0; i < 8; i++) + // rgmixgroupid[i] = -1; + // rgmixgroupid[0] = prule->mixgroupid; + // float vol = MXR_GetVolFromMixGroup( rgmixgroupid ); + // DevMsg("%s(%1.2f) ", prule->szmixgroup, vol); + DevMsg("%s ", prule->szmixgroup); + } + } + } +} + +struct debug_showvols_t +{ + char *psz; // group name + int mixgroupid; // groupid + float vol; // group volume + float totalvol; // total volume of all sounds playing in this group +}; + + +// display routine for MXR_DebugShowMixVolumes + +#define MXR_DEBUG_INCY (1.0/40.0) // vertical text spacing +#define MXR_DEBUG_GREENSTART 0.3 // start position on screen of bar + +#define MXR_DEBUG_MAXVOL 1.0 // max volume scale +#define MXR_DEBUG_REDLIMIT 1.0 // volume limit into yellow +#define MXR_DEBUG_YELLOWLIMIT 0.7 // volume limit into red + +#define MXR_DEBUG_VOLSCALE 48 // length of graph in characters +#define MXR_DEBUG_CHAR '-' // bar character + +extern ConVar dsp_volume; +int g_debug_mxr_displaycount = 0; + +void MXR_DebugGraphMixVolumes( debug_showvols_t *groupvols, int cgroups) +{ + float flXpos, flYpos, flXposBar, duration; + int r,g,b,a; + int rb, gb, bb, ab; + flXpos = 0; + flYpos = 0; + char text[128]; + char bartext[MXR_DEBUG_VOLSCALE*3]; + + duration = 0.01; + + g_debug_mxr_displaycount++; + + if (!(g_debug_mxr_displaycount % 10)) + return; // only display every 10 frames + + + r = 96; g = 86; b = 226; a = 255; ab = 255; + + // show volume, dsp_volume + + Q_snprintf( text, 128, "Game Volume: %1.2f", volume.GetFloat()); + CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); + flYpos += MXR_DEBUG_INCY; + + Q_snprintf( text, 128, "DSP Volume: %1.2f", dsp_volume.GetFloat()); + CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); + flYpos += MXR_DEBUG_INCY; + + for (int i = 0; i < cgroups; i++) + { + // r += 64; g += 64; b += 16; + + r = r % 255; g = g % 255; b = b % 255; + + Q_snprintf( text, 128, "%s: %1.2f (%1.2f)", groupvols[i].psz, + groupvols[i].vol * g_DuckScale, groupvols[i].totalvol * g_DuckScale); + + CDebugOverlay::AddScreenTextOverlay(flXpos, flYpos, duration, r, g, b,a, text); + + // draw volume bar graph + + float vol = (groupvols[i].totalvol * g_DuckScale) / MXR_DEBUG_MAXVOL; + + // draw first 70% green + float vol1 = 0.0; + float vol2 = 0.0; + float vol3 = 0.0; + int cbars; + + vol1 = clamp(vol, 0.0f, 0.7f); + vol2 = clamp(vol, 0.0f, 0.95f); + vol3 = vol; + + flXposBar = flXpos + MXR_DEBUG_GREENSTART; + + if (vol1 > 0.0) + { + //flXposBar = flXpos + MXR_DEBUG_GREENSTART; + + rb = 0; gb= 255; bb = 0; // green bar + Q_memset(bartext, 0, sizeof(bartext)); + + cbars = (int)((float)vol1 * (float)MXR_DEBUG_VOLSCALE); + cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); + Q_memset(bartext, MXR_DEBUG_CHAR, cbars); + + CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); + } + + + // yellow bar + if (vol2 > MXR_DEBUG_YELLOWLIMIT) + { + rb = 255; gb = 255; bb = 0; + Q_memset(bartext, 0, sizeof(bartext)); + + cbars = (int)((float)vol2 * (float)MXR_DEBUG_VOLSCALE); + cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); + Q_memset(bartext, MXR_DEBUG_CHAR, cbars); + + CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); + } + + // red bar + if (vol3 > MXR_DEBUG_REDLIMIT) + { + //flXposBar = flXpos + MXR_DEBUG_REDSTART; + rb = 255; gb = 0; bb = 0; + Q_memset(bartext, 0, sizeof(bartext)); + + cbars = (int)((float)vol3 * (float)MXR_DEBUG_VOLSCALE); + cbars = clamp(cbars, 0, MXR_DEBUG_VOLSCALE*3-1); + Q_memset(bartext, MXR_DEBUG_CHAR, cbars); + + CDebugOverlay::AddScreenTextOverlay(flXposBar, flYpos, duration, rb, gb, bb,ab, bartext); + } + + flYpos += MXR_DEBUG_INCY; + } +} + +ConVar snd_disable_mixer_duck("snd_disable_mixer_duck", "0"); // if 1, soundmixer ducking is disabled + +// given mix group id, return current duck volume + +float MXR_GetDuckVolume( int mixgroupid ) +{ + + if ( snd_disable_mixer_duck.GetInt() ) + return 1.0; + + Assert ( mixgroupid < g_cgrouprules ); + + int grouprulesid = g_mapMixgroupidToGrouprulesid[mixgroupid]; + + // if this mixgroup is not ducked, return 1.0 + + if ( !g_grouprules[grouprulesid].is_ducked ) + return 1.0; + + // return current duck value for this group, scaled by current fade in/out ramp + + return g_grouprules[grouprulesid].duck_ramp_val; + +} + +#define SND_DUCKER_UPDATETIME 0.1 // seconds to wait between ducker updates + +double g_mxr_ducktime = 0.0; // time of last update to ducker + +// Get total volume currently playing in all groups, +// process duck volumes for all groups +// Call once per frame - updates occur at 10hz + +void MXR_UpdateAllDuckerVolumes( void ) +{ + if ( snd_disable_mixer_duck.GetInt() ) + return; + + // check timer since last update, only update at 10hz + + int i; + double dtime = g_pSoundServices->GetHostTime(); + + // don't update until timer expires + + if (fabs(dtime - g_mxr_ducktime) < SND_DUCKER_UPDATETIME) + return; + + g_mxr_ducktime = dtime; + + // clear out all total volume values for groups + + for ( i = 0; i < g_cgrouprules; i++) + g_grouprules[i].total_vol = 0.0; + + // for every channel in a mix group which can cause ducking: + // get total volume, store total in grouprule: + + CChannelList list; + int ch_idx; + + channel_t *pchan; + bool b_found_ducked_channel = false; + + g_ActiveChannels.GetActiveChannels( list ); + + for ( i = 0; i < list.Count(); i++ ) + { + ch_idx = list.GetChannelIndex(i); + pchan = &channels[ch_idx]; + + if (pchan->last_vol > 0.0) + { + // account for all mix groups this channel belongs to... + + for (int j = 0; j < 8; j++) + { + int imixgroup = pchan->mixgroups[j]; + + if (imixgroup < 0) + continue; + + int grouprulesid = g_mapMixgroupidToGrouprulesid[imixgroup]; + + if (g_grouprules[grouprulesid].causes_ducking) + g_grouprules[grouprulesid].total_vol += pchan->last_vol; + + if (g_grouprules[grouprulesid].is_ducked) + b_found_ducked_channel = true; + } + } + } + + // if no channels playing which may be ducked, do nothing + + if ( !b_found_ducked_channel ) + return; + + // for all groups that can be ducked: + // see if a higher priority sound group has a volume > threshold, + // if so, then duck this group by setting duck_target_vol to duck_target_pct. + // if no sound group is causing ducking in this group, reset duck_target_vol to 1.0 + + for (i = 0; i < g_cgrouprules; i++) + { + if (g_grouprules[i].is_ducked) + { + int priority = g_grouprules[i].priority; + + float duck_volume = 1.0; // clear to 1.0 if no channel causing ducking + + // make sure we interact appropriately with global voice ducking... + // if global voice ducking is active, skip sound group ducking and just set duck_volume target to 1.0 + + if ( g_DuckScale >= 1.0 ) + { + // check all sound groups for higher priority duck trigger + + for (int j = 0; j < g_cgrouprules; j++) + { + if (g_grouprules[j].priority > priority && + g_grouprules[j].causes_ducking && + g_grouprules[j].total_vol > g_grouprules[j].ducker_threshold) + { + // a higher priority group is causing this group to be ducked + // set duck volume target to the ducked group's duck target percent + // and break + + duck_volume = g_grouprules[i].duck_target_pct; + + // UNDONE: to prevent edge condition caused by crossing threshold, may need to have secondary + // UNDONE: timer which allows ducking at 0.2 hz + + break; + } + } + } + + g_grouprules[i].duck_target_vol = duck_volume; + } + } + + // update all ducker ramps if current duck value is not target + // if ramp is greater than duck_volume, approach at 'attack rate' + // if ramp is less than duck_volume, approach at 'decay rate' + + for (i = 0; i < g_cgrouprules; i++) + { + float target = g_grouprules[i].duck_target_vol; + float current = g_grouprules[i].duck_ramp_val; + + if (g_grouprules[i].is_ducked && (current != target)) + { + + float ramptime = target < current ? snd_duckerattacktime.GetFloat() : snd_duckerreleasetime.GetFloat(); + + // delta is volume change per update (we can do this + // since we run at an approximate fixed update rate of 10hz) + + float delta = (1.0 - g_grouprules[i].duck_target_pct); + + delta *= ( SND_DUCKER_UPDATETIME / ramptime ); + + if (current > target) + delta = -delta; + + // update ramps + + current += delta; + + if (current < target && delta < 0) + current = target; + if (current > target && delta > 0) + current = target; + + g_grouprules[i].duck_ramp_val = current; + } + } + +} + +ConVar snd_showmixer("snd_showmixer", "0"); // set to 1 to show mixer every frame + +// show the current soundmixer output + +void MXR_DebugShowMixVolumes( void ) +{ + if (snd_showmixer.GetInt() == 0) + return; + + // for the current soundmixer: + // make a totalvolume bucket for each mixgroup type in the soundmixer. + // for every active channel, add its spatialized volume to + // totalvolume bucket for that channel's selected mixgroup + + // display all mixgroup/volume/totalvolume values as horizontal bars + + debug_showvols_t groupvols[CMXRGROUPMAX]; + + int i; + int cgroups = 0; + + if (g_isoundmixer < 0) + { + DevMsg("No sound mixer selected!"); + return; + } + + soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; + + // for every entry in mapMixgroupidToValue which is not -1, + // set up groupvols + + for (i = 0; i < CMXRGROUPMAX; i++) + { + if (pmixer->mapMixgroupidToValue[i] >= 0) + { + groupvols[cgroups].mixgroupid = i; + groupvols[cgroups].psz = MXR_GetGroupnameFromId( i ); + groupvols[cgroups].totalvol = 0.0; + groupvols[cgroups].vol = pmixer->mapMixgroupidToValue[i]; + cgroups++; + } + } + + // for every active channel, get its volume and + // the selected mixgroupid, add to groupvols totalvol + + CChannelList list; + int ch_idx; + channel_t *pchan; + + g_ActiveChannels.GetActiveChannels( list ); + + for ( i = 0; i < list.Count(); i++ ) + { + ch_idx = list.GetChannelIndex(i); + pchan = &channels[ch_idx]; + if (pchan->last_vol > 0.0) + { + // find entry in groupvols + for (int j = 0; j < CMXRGROUPMAX; j++) + { + if (pchan->last_mixgroupid == groupvols[j].mixgroupid) + { + groupvols[j].totalvol += pchan->last_vol; + break; + } + } + } + } + + // groupvols is now fully initialized - just display it + + MXR_DebugGraphMixVolumes( groupvols, cgroups); +} + +#ifdef _DEBUG + +// set the named mixgroup volume to vol for the current soundmixer +static void MXR_DebugSetMixGroupVolume( const CCommand &args ) +{ + if ( args.ArgC() != 3 ) + { + DevMsg("Parameters: mix group name, volume"); + return; + } + + const char *szgroupname = args[1]; + float vol = atof( args[2] ); + + int imixgroup = MXR_GetMixgroupFromName( szgroupname ); + + if ( g_isoundmixer < 0 ) + return; + + soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; + + pmixer->mapMixgroupidToValue[imixgroup] = vol; +} + +#endif //_DEBUG + +// given array of groupids (ie: the sound is in these groups), +// return a mix volume. + +// return first mixgroup id in the provided array +// which maps to a non -1 volume value for this +// sound mixer + +float MXR_GetVolFromMixGroup( int rgmixgroupid[8], int *plast_mixgroupid ) +{ + + // if no soundmixer currently set, return 1.0 volume + + if (g_isoundmixer < 0) + { + *plast_mixgroupid = 0; + return 1.0; + } + + float duckgain = 1.0; + + if (g_csoundmixers) + { + soundmixer_t *pmixer = &g_soundmixers[g_isoundmixer]; + + if (pmixer) + { + // search mixgroupid array, return first match (non -1) + + for (int i = 0; i < 8; i++) + { + int imixgroup = rgmixgroupid[i]; + + if (imixgroup < 0) + continue; + + // save lowest duck gain value for any of the mix groups this sound is in + + float duckgain_new = MXR_GetDuckVolume( imixgroup ); + + if ( duckgain_new < duckgain) + duckgain = duckgain_new; + + Assert(imixgroup < CMXRGROUPMAX); + + // return first mixgroup id in the passed in array + // that maps to a non -1 volume value for this + // sound mixer + + if ( pmixer->mapMixgroupidToValue[imixgroup] >= 0) + { + *plast_mixgroupid = imixgroup; + + // get gain due to mixer settings + + float gain = pmixer->mapMixgroupidToValue[imixgroup]; + + // modify gain with ducker settings for this group + + return gain * duckgain; + } + } + } + } + + *plast_mixgroupid = 0; + return duckgain; +} + +// get id of mixgroup name + +int MXR_GetMixgroupFromName( const char *pszgroupname ) +{ + // scan group rules for mapping from name to id + if ( !pszgroupname ) + return -1; + + if ( Q_strlen(pszgroupname) == 0 ) + return -1; + + for (int i = 0; i < g_cgrouprules; i++) + { + if ( !Q_stricmp(g_grouprules[i].szmixgroup, pszgroupname ) ) + return g_grouprules[i].mixgroupid; + } + + return -1; +} + +// get mixgroup name from id +char *MXR_GetGroupnameFromId( int mixgroupid) +{ + // scan group rules for mapping from name to id + if (mixgroupid < 0) + return NULL; + + for (int i = 0; i < g_cgrouprules; i++) + { + if ( g_grouprules[i].mixgroupid == mixgroupid) + return g_grouprules[i].szmixgroup; + } + + return NULL; +} + +// assign a unique mixgroup id to each unique named mix group +// within grouprules. Note: all mixgroupids in grouprules must be -1 +// when this routine starts. + +void MXR_AssignGroupIds( void ) +{ + int cmixgroupid = 0; + + for (int i = 0; i < g_cgrouprules; i++) + { + int mixgroupid = MXR_GetMixgroupFromName( g_grouprules[i].szmixgroup ); + + if (mixgroupid == -1) + { + // groupname is not yet assigned, provide a unique mixgroupid. + + g_grouprules[i].mixgroupid = cmixgroupid; + + // save reverse mapping, from mixgroupid to the first grouprules entry for this name + + g_mapMixgroupidToGrouprulesid[cmixgroupid] = i; + + cmixgroupid++; + } + } +} + +int MXR_AddClassname( const char *pName ) +{ + char szclassname[CMXRNAMEMAX]; + Q_strncpy( szclassname, pName, CMXRNAMEMAX ); + for ( int i = 0; i < g_cgroupclass; i++ ) + { + if ( !Q_stricmp( szclassname, g_groupclasslist[i].szclassname ) ) + return i; + } + if ( g_cgroupclass >= CMXRCLASSMAX ) + { + Assert(g_cgroupclass < CMXRCLASSMAX); + return -1; + } + Q_memcpy(g_groupclasslist[g_cgroupclass].szclassname, pName, min((size_t)CMXRNAMEMAX-1, strlen(pName))); + g_cgroupclass++; + return g_cgroupclass-1; +} + +#define CHAR_LEFT_PAREN '{' +#define CHAR_RIGHT_PAREN '}' + +// load group rules and sound mixers from file + +bool MXR_LoadAllSoundMixers( void ) +{ + // init soundmixer globals + + g_isoundmixer = -1; + g_szsoundmixer_cur[0] = 0; + + g_csoundmixers = 0; // total number of soundmixers found + g_cgrouprules = 0; // total number of group rules found + + Q_memset(g_soundmixers, 0, sizeof(g_soundmixers)); + Q_memset(g_grouprules, 0, sizeof(g_grouprules)); + + // load file + + // build rules + + // build array of sound mixers + + char szFile[MAX_OSPATH]; + const char *pstart; + bool bResult = false; + char *pbuffer; + + Q_snprintf( szFile, sizeof( szFile ), "scripts/soundmixers.txt" ); + + pbuffer = (char *)COM_LoadFile( szFile, 5, NULL ); // Use malloc - free at end of this routine + if ( !pbuffer ) + { + Error( "MXR_LoadAllSoundMixers: unable to open '%s'\n", szFile ); + return bResult; + } + + pstart = pbuffer; + + // first pass: load g_grouprules[] + + // starting at top of file, + // scan for first '{', skipping all comment lines + // get strings for: groupname, directory, classname, chan, sndlvl_min, sndlvl_max + // convert chan to CHAN_ lookup + // convert sndlvl_min, sndl_max to ints + // store all in g_grouprules, update g_cgrouprules; + // get next line + // when hit '}' we're done with grouprules + + // check for first CHAR_LEFT_PAREN + + while (1) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; // eof + + if ( com_token[0] != CHAR_LEFT_PAREN ) + continue; + + break; + } + + while (1) + { + pstart = COM_Parse( pstart ); + if (com_token[0] == CHAR_RIGHT_PAREN) + break; + + grouprule_t *pgroup = &g_grouprules[g_cgrouprules]; + + // copy mixgroup name, directory, classname + // if no value specified, set to 0 length string + + if (com_token[0]) + Q_memcpy(pgroup->szmixgroup, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); + + pstart = COM_Parse( pstart ); + if (com_token[0]) + Q_memcpy(pgroup->szdir, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); + + pgroup->classId = -1; + pstart = COM_Parse( pstart ); + if (com_token[0]) + { + pgroup->classId = MXR_AddClassname( com_token ); + } + + // make sure all copied strings are null terminated + pgroup->szmixgroup[CMXRNAMEMAX-1] = 0; + pgroup->szdir[CMXRNAMEMAX-1] = 0; + + // lookup chan + pstart = COM_Parse( pstart ); + if (com_token[0]) + { + if (!Q_stricmp(com_token, "CHAN_STATIC")) + pgroup->chantype = CHAN_STATIC; + else if (!Q_stricmp(com_token, "CHAN_WEAPON")) + pgroup->chantype = CHAN_WEAPON; + else if (!Q_stricmp(com_token, "CHAN_VOICE")) + pgroup->chantype = CHAN_VOICE; + else if (!Q_stricmp(com_token, "CHAN_VOICE2")) + pgroup->chantype = CHAN_VOICE2; + else if (!Q_stricmp(com_token, "CHAN_BODY")) + pgroup->chantype = CHAN_BODY; + else if (!Q_stricmp(com_token, "CHAN_ITEM")) + pgroup->chantype = CHAN_ITEM; + } + else + pgroup->chantype = -1; + + // get sndlvls + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->soundlevel_min = atoi(com_token); + else + pgroup->soundlevel_min = -1; + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->soundlevel_max = atoi(com_token); + else + pgroup->soundlevel_max = -1; + + // get duck priority, IsDucked, Causes_ducking, duck_target_pct + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->priority = atoi(com_token); + else + pgroup->priority = 50; + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->is_ducked = atoi(com_token); + else + pgroup->is_ducked = 0; + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->causes_ducking = atoi(com_token); + else + pgroup->causes_ducking = 0; + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->duck_target_pct = ((float)(atoi(com_token))) / 100.0f; + else + pgroup->duck_target_pct = 0.5f; + + pstart = COM_Parse( pstart ); + if (com_token[0]) + pgroup->ducker_threshold = ((float)(atoi(com_token))) / 100.0f; + else + pgroup->ducker_threshold = 0.5f; + + pgroup->duck_ramp_val = 1.0; + pgroup->duck_target_vol = 1.0; + pgroup->total_vol = 0.0; + + // set mixgroup id to -1 + pgroup->mixgroupid = -1; + + // update rule count + + g_cgrouprules++; + + if (g_cgrouprules >= CMXRGROUPRULESMAX) + { + // UNDONE: error! too many rules + break; + } + } + + // now process all groupids in groups, such that + // each mixgroup gets a unique id. + + MXR_AssignGroupIds(); + + // now load g_soundmixers + + // while not at end of file... + // scan for "<name>", if found save as new soundmixer name + // while not '}' + // scan for "<name>", save as groupname + // scan for "<num>", save as mix value + + while(1) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; // eof + + // save name in soundmixer + + soundmixer_t *pmixer = &g_soundmixers[g_csoundmixers]; + + Q_memcpy(pmixer->szsoundmixer, com_token, min((size_t)CMXRNAMEMAX-1, strlen(com_token))); + + // init all mixer values to -1. + + for (int j = 0; j < CMXRGROUPMAX; j++) + { + pmixer->mapMixgroupidToValue[j] = -1.0; + } + + // load all groupnames for this soundmixer + + while (1) + { + pstart = COM_Parse( pstart ); + + if (com_token[0] == CHAR_LEFT_PAREN) + continue; // skip { + + if (com_token[0] == CHAR_RIGHT_PAREN) + break; // finished with this sounmixer + + // lookup mixgroupid for groupname + int mixgroupid = MXR_GetMixgroupFromName( com_token ); + float value; + + // get mix value + pstart = COM_Parse( pstart ); + value = atof( com_token ); + + // store value for mixgroupid + Assert(mixgroupid <= CMXRGROUPMAX); + + pmixer->mapMixgroupidToValue[mixgroupid] = value; + } + + g_csoundmixers++; + if (g_csoundmixers >= CMXRSOUNDMIXERSMAX) + { + // UNDONE: error! to many sound mixers + break; + } + } + + bResult = true; + +// loadmxr_exit: + free( pbuffer ); + return bResult; +} + +void MXR_ReleaseMemory( void ) +{ + // free all resources +} + +float S_GetMono16Samples( const char *pszName, CUtlVector< short >& sampleList ) +{ + CSfxTable *pSfx = S_PrecacheSound( PSkipSoundChars( pszName ) ); + if ( !pSfx ) + return 0.0f; + + CAudioSource *pWave = pSfx->pSource; + if ( !pWave ) + return 0.0f; + + int nType = pWave->GetType(); + if ( nType != CAudioSource::AUDIO_SOURCE_WAV ) + return 0.0f; + + CAudioMixer *pMixer = pWave->CreateMixer(); + if ( !pMixer ) + return 0.0f; + + float duration = AudioSource_GetSoundDuration( pSfx ); + + // Determine start/stop positions + int totalsamples = (int)( duration * pWave->SampleRate() ); + if ( totalsamples <= 0 ) + return 0; + + bool bStereo = pWave->IsStereoWav(); + int mix_sample_size = pMixer->GetMixSampleSize(); + int nNumChannels = bStereo ? 2 : 1; + + char *pData = NULL; + + int pos = 0; + int remaining = totalsamples; + while ( remaining > 0 ) + { + int blockSize = min( remaining, 1000 ); + + char copyBuf[AUDIOSOURCE_COPYBUF_SIZE]; + int copied = pWave->GetOutputData( (void **)&pData, pos, blockSize, copyBuf ); + if ( !copied ) + { + break; + } + + remaining -= copied; + pos += copied; + + // Now get samples out of output data + switch ( nNumChannels ) + { + default: + case 1: + { + for ( int i = 0; i < copied; ++i ) + { + int offset = i * mix_sample_size; + + short sample = 0; + if ( mix_sample_size == 1 ) + { + char s = *( char * )( pData + offset ); + // Upscale it to fit into a short + sample = s << 8; + } + else if ( mix_sample_size == 2 ) + { + sample = *( short * )( pData + offset ); + } + else if ( mix_sample_size == 4 ) + { + // Not likely to have 4 bytes mono!!! + Assert( 0 ); + + int s = *( int * )( pData + offset ); + sample = s >> 16; + } + else + { + Assert( 0 ); + } + + sampleList.AddToTail( sample ); + } + } + break; + + case 2: + { + for ( int i = 0; i < copied; ++i ) + { + int offset = i * mix_sample_size; + + short left = 0; + short right = 0; + + if ( mix_sample_size == 1 ) + { + // Not possible!!!, must be at least 2 bytes!!! + Assert( 0 ); + + char v = *( char * )( pData + offset ); + left = right = ( v << 8 ); + } + else if ( mix_sample_size == 2 ) + { + // One byte per channel + left = (short)( ( *(char *)( pData + offset ) ) << 8 ); + right = (short)( ( *(char *)( pData + offset + 1 ) ) << 8 ); + } + else if ( mix_sample_size == 4 ) + { + // 2 bytes per channel + left = *( short * )( pData + offset ); + right = *( short * )( pData + offset + 2 ); + } + else + { + Assert( 0 ); + } + + short sample = ( left + right ) >> 1; + sampleList.AddToTail( sample ); + } + } + break; + } + } + + delete pMixer; + + return duration; +} diff --git a/engine/audio/private/snd_dma.h b/engine/audio/private/snd_dma.h new file mode 100644 index 0000000..30d3552 --- /dev/null +++ b/engine/audio/private/snd_dma.h @@ -0,0 +1,19 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_DMA_H +#define SND_DMA_H +#ifdef _WIN32 +#pragma once +#endif + + +extern bool snd_initialized; + +bool SND_IsInGame( void ); + +#endif // SND_DMA_H diff --git a/engine/audio/private/snd_dsp.cpp b/engine/audio/private/snd_dsp.cpp new file mode 100644 index 0000000..4e73fdb --- /dev/null +++ b/engine/audio/private/snd_dsp.cpp @@ -0,0 +1,9679 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// snd_dsp.c -- audio processing routines + + +#include "audio_pch.h" +#include "snd_mix_buf.h" + +#include "iprediction.h" +#include "../../common.h" // for parsing routines +#include "vstdlib/random.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#define SIGN(d) ((d)<0?-1:1) + +#define ABS(a) abs(a) + +#define MSEC_TO_SAMPS(a) (((a)*SOUND_DMA_SPEED) / 1000) // convert milliseconds to # samples in equivalent time +#define SEC_TO_SAMPS(a) ((a)*SOUND_DMA_SPEED) // convert seconds to # samples in equivalent time + +// Suppress the noisy warnings caused by CLIP_DSP +#if defined(__clang__) + #pragma GCC diagnostic ignored "-Wself-assign" +#endif +#define CLIP_DSP(x) (x) + +extern ConVar das_debug; + +#define SOUND_MS_PER_FT 1 // sound travels approx 1 foot per millisecond +#define ROOM_MAX_SIZE 1000 // max size in feet of room simulation for dsp + +void DSP_ReleaseMemory( void ); +bool DSP_LoadPresetFile( void ); + +extern float Gain_To_dB ( float gain ); +extern float dB_To_Gain ( float dB ); +extern float Gain_To_Amplitude ( float gain ); +extern float Amplitude_To_Gain ( float amplitude ); + +extern bool g_bdas_room_init; +extern bool g_bdas_init_nodes; + +//=============================================================================== +// +// Digital Signal Processing algorithms for audio FX. +// +// KellyB 2/18/03 +//=============================================================================== + +// Performance notes: + +// DSP processing should take no more than 3ms total time per frame to remain on par with hl1 +// Assume a min frame rate of 24fps = 42ms per frame +// at 24fps, to maintain 44.1khz output rate, we must process about 1840 mono samples per frame. +// So we must process 1840 samples in 3ms. + +// on a 1Ghz CPU (mid-low end CPU) 3ms provides roughly 3,000,000 cycles. +// Thus we have 3e6 / 1840 = 1630 cycles per sample. + +#define PBITS 12 // parameter bits +#define PMAX ((1 << PBITS)) // parameter max + +// crossfade from y2 to y1 at point r (0 < r < PMAX) + +#define XFADE(y1,y2,r) ((y2) + ( ( ((y1) - (y2)) * (r) ) >> PBITS) ) + +// exponential crossfade from y2 to y1 at point r (0 < r < PMAX) + +#define XFADE_EXP(y1, y2, r) ((y2) + ((((((y1) - (y2)) * (r) ) >> PBITS) * (r)) >> PBITS) ) + +///////////////////// +// dsp helpers +///////////////////// + +// reverse delay pointer + +inline void DlyPtrReverse (int dlysize, int *psamps, int **ppsamp) +{ + // when *ppsamp = psamps - 1, it wraps around to *ppsamp = psamps + dlysize + + if ( *ppsamp < psamps ) + *ppsamp += dlysize + 1; +} + +// advance delay pointer + +inline void DlyPtrForward (int dlysize, int *psamps, int **ppsamp) +{ + // when *ppsamp = psamps + dlysize + 1, it wraps around to *ppsamp = psamps + + if ( *ppsamp > psamps + dlysize ) + *ppsamp -= dlysize + 1; +} + +// Infinite Impulse Response (feedback) filter, cannonical form + +// returns single sample 'out' for current input value 'in' +// in: input sample +// psamp: internal state array, dimension max(cdenom,cnumer) + 1 +// cnumer,cdenom: numerator and denominator filter orders +// denom,numer: cdenom+1 dimensional arrays of filter params +// +// for cdenom = 4: +// +// 1 psamp0(n) numer0 +// in(n)--->(+)--(*)---.------(*)---->(+)---> out(n) +// ^ | ^ +// | [Delay d] | +// | | | +// | -denom1 |psamp1 numer1 | +// ----(*)---.------(*)------- +// ^ | ^ +// | [Delay d] | +// | | | +// | -denom2 |psamp2 numer2 | +// ----(*)---.------(*)------- +// ^ | ^ +// | [Delay d] | +// | | | +// | -denom3 |psamp3 numer3 | +// ----(*)---.------(*)------- +// ^ | ^ +// | [Delay d] | +// | | | +// | -denom4 |psamp4 numer4 | +// ----(*)---.------(*)------- +// +// for each input sample in: +// psamp0 = in - denom1*psamp1 - denom2*psamp2 - ... +// out = numer0*psamp0 + numer1*psamp1 + ... +// psampi = psampi-1, i = cmax, cmax-1, ..., 1 + +inline int IIRFilter_Update_OrderN ( int cdenom, int *denom, int cnumer, int *numer, int *psamp, int in ) +{ + int cmax, i; + int out; + int in0; + + out = 0; + in0 = in; + + cmax = max ( cdenom, cnumer ); + + // add input values + + // for (i = 1; i <= cdenom; i++) + // psamp[0] -= ( denom[i] * psamp[i] ) >> PBITS; + + switch (cdenom) + { + case 12: in0 -= ( denom[12] * psamp[12] ) >> PBITS; + case 11: in0 -= ( denom[11] * psamp[11] ) >> PBITS; + case 10: in0 -= ( denom[10] * psamp[10] ) >> PBITS; + case 9: in0 -= ( denom[9] * psamp[9] ) >> PBITS; + case 8: in0 -= ( denom[8] * psamp[8] ) >> PBITS; + case 7: in0 -= ( denom[7] * psamp[7] ) >> PBITS; + case 6: in0 -= ( denom[6] * psamp[6] ) >> PBITS; + case 5: in0 -= ( denom[5] * psamp[5] ) >> PBITS; + case 4: in0 -= ( denom[4] * psamp[4] ) >> PBITS; + case 3: in0 -= ( denom[3] * psamp[3] ) >> PBITS; + case 2: in0 -= ( denom[2] * psamp[2] ) >> PBITS; + default: + case 1: in0 -= ( denom[1] * psamp[1] ) >> PBITS; + } + + psamp[0] = in0; + + // add output values + + //for (i = 0; i <= cnumer; i++) + // out += ( numer[i] * psamp[i] ) >> PBITS; + + switch (cnumer) + { + case 12: out += ( numer[12] * psamp[12] ) >> PBITS; + case 11: out += ( numer[11] * psamp[11] ) >> PBITS; + case 10: out += ( numer[10] * psamp[10] ) >> PBITS; + case 9: out += ( numer[9] * psamp[9] ) >> PBITS; + case 8: out += ( numer[8] * psamp[8] ) >> PBITS; + case 7: out += ( numer[7] * psamp[7] ) >> PBITS; + case 6: out += ( numer[6] * psamp[6] ) >> PBITS; + case 5: out += ( numer[5] * psamp[5] ) >> PBITS; + case 4: out += ( numer[4] * psamp[4] ) >> PBITS; + case 3: out += ( numer[3] * psamp[3] ) >> PBITS; + case 2: out += ( numer[2] * psamp[2] ) >> PBITS; + default: + case 1: out += ( numer[1] * psamp[1] ) >> PBITS; + case 0: out += ( numer[0] * psamp[0] ) >> PBITS; + } + + // update internal state (reverse order) + + for (i = cmax; i >= 1; i--) + psamp[i] = psamp[i-1]; + + // return current output sample + + return out; +} + +// 1st order filter - faster version + +inline int IIRFilter_Update_Order1 ( int *denom, int cnumer, int *numer, int *psamp, int in ) +{ + int out; + + if (!psamp[0] && !psamp[1] && !in) + return 0; + + psamp[0] = in - (( denom[1] * psamp[1] ) >> PBITS); + + out = ( ( numer[1] * psamp[1] ) + ( numer[0] * psamp[0] ) ) >> PBITS; + + psamp[1] = psamp[0]; + + return out; +} + +// return 'tdelay' delayed sample from delay buffer +// dlysize: delay samples +// psamps: head of delay buffer psamps[0...dlysize] +// psamp: current data pointer +// sdly: 0...dlysize + +inline int GetDly ( int dlysize, int *psamps, int *psamp, int tdelay ) +{ + int *pout; + + pout = psamp + tdelay; + + if ( pout <= (psamps + dlysize)) + return *pout; + else + return *(pout - dlysize - 1); +} + +// update the delay buffer pointer +// dlysize: delay samples +// psamps: head of delay buffer psamps[0...dlysize] +// ppsamp: data pointer + +inline void DlyUpdate ( int dlysize, int *psamps, int **ppsamp ) +{ + // decrement pointer and fix up on buffer boundary + + // when *ppsamp = psamps-1, it wraps around to *ppsamp = psamps+dlysize + + (*ppsamp)--; + DlyPtrReverse ( dlysize, psamps, ppsamp ); +} + +// simple delay with feedback, no filter in feedback line. +// delaysize: delay line size in samples +// tdelay: tap from this location - <= delaysize +// psamps: delay line buffer pointer of dimension delaysize+1 +// ppsamp: circular pointer, must be init to &psamps[0] before first call +// fbgain: feedback value, 0-PMAX (normalized to 0.0-1.0) +// outgain: gain +// in: input sample + +// psamps0(n) outgain +// in(n)--->(+)--------.-----(*)-> out(n) +// ^ | +// | [Delay d] +// | | +// | fbgain |Wd(n) +// ----(*)---. + +inline int ReverbSimple ( int delaysize, int tdelay, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int out, sD; + + // get current delay output + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + + // calculate output + delay * gain + + out = in + (( fbgain * sD ) >> PBITS); + + // write to delay + + **ppsamp = out; + + // advance internal delay pointers + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( (out * outgain) >> PBITS ); +} + +inline int ReverbSimple_xfade ( int delaysize, int tdelay, int tdelaynew, int xf, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int out, sD; + int sDnew; + + // crossfade from tdelay to tdelaynew samples. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + sD = sD + (((sDnew - sD) * xf) >> PBITS); + + out = in + (( fbgain * sD ) >> PBITS); + **ppsamp = out; + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( (out * outgain) >> PBITS ); +} + +// multitap simple reverb + +// NOTE: tdelay3 > tdelay2 > tdelay1 > t0 +// NOTE: fbgain * 4 < 1! + +inline int ReverbSimple_multitap ( int delaysize, int tdelay0, int tdelay1, int tdelay2, int tdelay3, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int s1, s2, s3, s4, sum; + + s1 = GetDly ( delaysize, psamps, *ppsamp, tdelay0 ); + s2 = GetDly ( delaysize, psamps, *ppsamp, tdelay1 ); + s3 = GetDly ( delaysize, psamps, *ppsamp, tdelay2 ); + s4 = GetDly ( delaysize, psamps, *ppsamp, tdelay3 ); + + sum = s1 + s2 + s3 + s4; + + // write to delay + + **ppsamp = in + ((s4 * fbgain) >> PBITS); + + // update delay pointers + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( ((sum + in) * outgain ) >> PBITS ); +} + +// modulate smallest tap delay only + +inline int ReverbSimple_multitap_xfade ( int delaysize, int tdelay0, int tdelaynew, int xf, int tdelay1, int tdelay2, int tdelay3, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int s1, s2, s3, s4, sum; + int sD, sDnew; + + // crossfade from tdelay to tdelaynew tap. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay3 ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + + s4 = sD + (((sDnew - sD) * xf) >> PBITS); + + s1 = GetDly ( delaysize, psamps, *ppsamp, tdelay0 ); + s2 = GetDly ( delaysize, psamps, *ppsamp, tdelay1 ); + s3 = GetDly ( delaysize, psamps, *ppsamp, tdelay2 ); + + sum = s1 + s2 + s3 + s4; + + // write to delay + + **ppsamp = in + ((s4 * fbgain) >> PBITS); + + // update delay pointers + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( ((sum + in) * outgain ) >> PBITS ); +} + +// straight delay, no feedback +// +// delaysize: delay line size in samples +// tdelay: tap from this location - <= delaysize +// psamps: delay line buffer pointer of dimension delaysize+1 +// ppsamp: circular pointer, must be init to &psamps[0] before first call +// in: input sample +// +// in(n)--->[Delay d]---> out(n) +// + +inline int DelayLinear ( int delaysize, int tdelay, int *psamps, int **ppsamp, int in ) +{ + int out; + + out = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + + **ppsamp = in; + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( out ); +} + +// crossfade delay values from tdelay to tdelaynew, with xfade1 for tdelay and xfade2 for tdelaynew. xfade = 0...PMAX + +inline int DelayLinear_xfade ( int delaysize, int tdelay, int tdelaynew, int xf, int *psamps, int **ppsamp, int in ) +{ + int out; + int outnew; + + out = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + + outnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + + out = out + (((outnew - out) * xf) >> PBITS); + + **ppsamp = in; + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( out ); +} + +// lowpass reverberator, replace feedback multiplier 'fbgain' in +// reverberator with a low pass filter + +// delaysize: delay line size in samples +// tdelay: tap from this location - <= delaysize +// psamps: delay line buffer pointer of dimension delaysize+1 +// ppsamp: circular pointer, must be init to &w[0] before first call +// fbgain: feedback gain (built into filter gain) +// outgain: output gain +// cnumer: filter order +// numer: filter numerator, 0-PMAX (normalized to 0.0-1.0), cnumer+1 dimensional +// denom: filter denominator, 0-PMAX (normalized to 0.0-1.0), cnumer+1 dimensional +// pfsamps: filter state, cnumer+1 dimensional +// in: input sample + +// psamps0(n) outgain +// in(n)--->(+)--------------.----(*)--> out(n) +// ^ | +// | [Delay d] +// | | +// | fbgain |Wd(n) +// --(*)--[Filter])- + +inline int DelayLowpass ( int delaysize, int tdelay, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int Ll, int *numer, int *pfsamps, int in ) +{ + int out, sD; + + // delay output is filter input + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + + // filter output, with feedback 'fbgain' baked into filter params + + out = in + IIRFilter_Update_Order1 ( denom, Ll, numer, pfsamps, sD ); + + // write to delay + + **ppsamp = out; + + // update delay pointers + + DlyUpdate ( delaysize, psamps, ppsamp ); + + // output with gain + + return ( (out * outgain) >> PBITS ); +} + +inline int DelayLowpass_xfade ( int delaysize, int tdelay, int tdelaynew, int xf, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int Ll, int *numer, int *pfsamps, int in ) +{ + int out, sD; + int sDnew; + + // crossfade from tdelay to tdelaynew tap. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + sD = sD + (((sDnew - sD) * xf) >> PBITS); + + // filter output with feedback 'fbgain' baked into filter params + + out = in + IIRFilter_Update_Order1 ( denom, Ll, numer, pfsamps, sD ); + + // write to delay + + **ppsamp = out; + + // update delay ptrs + + DlyUpdate ( delaysize, psamps, ppsamp ); + + // output with gain + + return ( (out * outgain) >> PBITS ); +} + +// delay is multitap tdelay0,tdelay1,tdelay2,tdelay3 + +// NOTE: tdelay3 > tdelay2 > tdelay1 > tdelay0 +// NOTE: fbgain * 4 < 1! + +inline int DelayLowpass_multitap ( int delaysize, int tdelay0, int tdelay1, int tdelay2, int tdelay3, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int Ll, int *numer, int *pfsamps, int in ) +{ + int s0, s1, s2, s3, s4, sum; + + s1 = GetDly ( delaysize, psamps, *ppsamp, tdelay0 ); + s2 = GetDly ( delaysize, psamps, *ppsamp, tdelay1 ); + s3 = GetDly ( delaysize, psamps, *ppsamp, tdelay2 ); + s4 = GetDly ( delaysize, psamps, *ppsamp, tdelay3 ); + + sum = s1 + s2 + s3 + s4; + + s0 = in + IIRFilter_Update_Order1 ( denom, Ll, numer, pfsamps, s4 ); + + // write to delay + + **ppsamp = s0; + + // update delay ptrs + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( ((sum + in) * outgain ) >> PBITS ); +} + +inline int DelayLowpass_multitap_xfade ( int delaysize, int tdelay0, int tdelaynew, int xf, int tdelay1, int tdelay2, int tdelay3, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int Ll, int *numer, int *pfsamps, int in ) +{ + int s0, s1, s2, s3, s4, sum; + + int sD, sDnew; + + // crossfade from tdelay to tdelaynew tap. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay3 ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + + s4 = sD + (((sDnew - sD) * xf) >> PBITS); + + s1 = GetDly ( delaysize, psamps, *ppsamp, tdelay0 ); + s2 = GetDly ( delaysize, psamps, *ppsamp, tdelay1 ); + s3 = GetDly ( delaysize, psamps, *ppsamp, tdelay2 ); + + sum = s1 + s2 + s3 + s4; + + s0 = in + IIRFilter_Update_Order1 ( denom, Ll, numer, pfsamps, s4 ); + + **ppsamp = s0; + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( ((sum + in) * outgain ) >> PBITS ); +} + +// linear delay with lowpass filter on delay output and gain stage +// delaysize: delay line size in samples +// tdelay: delay tap from this location - <= delaysize +// psamps: delay line buffer pointer of dimension delaysize+1 +// ppsamp: circular pointer, must init &psamps[0] before first call +// fbgain: feedback gain (ignored) +// outgain: output gain +// cnumer: filter order +// numer: filter numerator, 0-PMAX (normalized to 0.0-1.0), cnumer+1 dimensional +// denom: filter denominator, 0-PMAX (normalized to 0.0-1.0), cnumer+1 dimensional +// pfsamps: filter state, cnumer+1 dimensional +// in: input sample + +// in(n)--->[Delay d]--->[Filter]-->(*outgain)---> out(n) + +inline int DelayLinear_lowpass ( int delaysize, int tdelay, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int cnumer, int *numer, int *pfsamps, int in ) +{ + int out, sD; + + // delay output is filter input + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + + // calc filter output + + out = IIRFilter_Update_Order1 ( denom, cnumer, numer, pfsamps, sD ); + + // input sample to delay input + + **ppsamp = in; + + // update delay pointers + + DlyUpdate ( delaysize, psamps, ppsamp ); + + // output with gain + + return ( (out * outgain) >> PBITS ); +} + +inline int DelayLinear_lowpass_xfade ( int delaysize, int tdelay, int tdelaynew, int xf, int *psamps, int **ppsamp, int fbgain, int outgain, int *denom, int cnumer, int *numer, int *pfsamps, int in ) +{ + int out, sD; + int sDnew; + + // crossfade from tdelay to tdelaynew tap. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + sD = sD + (((sDnew - sD) * xf) >> PBITS); + + out = IIRFilter_Update_Order1 ( denom, cnumer, numer, pfsamps, sD ); + + **ppsamp = in; + + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( (out * outgain) >> PBITS ); +} + + +// classic allpass reverb +// delaysize: delay line size in samples +// tdelay: tap from this location - <= D +// psamps: delay line buffer pointer of dimension delaysize+1 +// ppsamp: circular pointer, must be init to &psamps[0] before first call +// fbgain: feedback value, 0-PMAX (normalized to 0.0-1.0) +// outgain: gain + +// psamps0(n) -fbgain outgain +// in(n)--->(+)--------.-----(*)-->(+)--(*)-> out(n) +// ^ | ^ +// | [Delay d] | +// | | | +// | fbgain |psampsd(n) | +// ----(*)---.------------- +// +// for each input sample 'in': +// psamps0 = in + fbgain * psampsd +// y = -fbgain * psamps0 + psampsd +// delay (d, psamps) - psamps is the delay buffer array +// +// or, using circular delay, for each input sample 'in': +// +// Sd = GetDly (delaysize,psamps,ppsamp,delaysize) +// S0 = in + fbgain*Sd +// y = -fbgain*S0 + Sd +// *ppsamp = S0 +// DlyUpdate(delaysize, psamps, &ppsamp) + +inline int DelayAllpass ( int delaysize, int tdelay, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int out, s0, sD; + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + s0 = in + (( fbgain * sD ) >> PBITS); + + out = ( ( -fbgain * s0 ) >> PBITS ) + sD; + **ppsamp = s0; + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( (out * outgain) >> PBITS ); +} + + +inline int DelayAllpass_xfade ( int delaysize, int tdelay, int tdelaynew, int xf, int *psamps, int **ppsamp, int fbgain, int outgain, int in ) +{ + int out, s0, sD; + int sDnew; + + // crossfade from t to tnew tap. xfade is 0..PMAX + + sD = GetDly ( delaysize, psamps, *ppsamp, tdelay ); + sDnew = GetDly ( delaysize, psamps, *ppsamp, tdelaynew ); + sD = sD + (((sDnew - sD) * xf) >> PBITS); + + s0 = in + (( fbgain * sD ) >> PBITS); + + out = ( ( -fbgain * s0 ) >> PBITS ) + sD; + **ppsamp = s0; + DlyUpdate ( delaysize, psamps, ppsamp ); + + return ( (out * outgain) >> PBITS ); +} + +/////////////////////////////////////////////////////////////////////////////////// +// fixed point math for real-time wave table traversing, pitch shifting, resampling +/////////////////////////////////////////////////////////////////////////////////// + +#define FIX20_BITS 20 // 20 bits of fractional part +#define FIX20_SCALE (1 << FIX20_BITS) + +#define FIX20_INTMAX ((1 << (32 - FIX20_BITS))-1) // maximum step integer + +#define FLOAT_TO_FIX20(a) ((int)((a) * (float)FIX20_SCALE)) // convert float to fixed point +#define INT_TO_FIX20(a) (((int)(a)) << FIX20_BITS) // convert int to fixed point +#define FIX20_TO_FLOAT(a) ((float)(a) / (float)FIX20_SCALE) // convert fix20 to float +#define FIX20_INTPART(a) (((int)(a)) >> FIX20_BITS) // get integer part of fixed point +#define FIX20_FRACPART(a) ((a) - (((a) >> FIX20_BITS) << FIX20_BITS)) // get fractional part of fixed point + +#define FIX20_FRACTION(a,b) (FIX(a)/(b)) // convert int a to fixed point, divide by b + +typedef int fix20int; + +///////////////////////////////// +// DSP processor parameter block +///////////////////////////////// + +// NOTE: these prototypes must match the XXX_Params ( prc_t *pprc ) and XXX_GetNext ( XXX_t *p, int x ) functions + +typedef void * (*prc_Param_t)( void *pprc ); // individual processor allocation functions +typedef int (*prc_GetNext_t) ( void *pdata, int x ); // get next function for processor +typedef int (*prc_GetNextN_t) ( void *pdata, portable_samplepair_t *pbuffer, int SampleCount, int op); // batch version of getnext +typedef void (*prc_Free_t) ( void *pdata ); // free function for processor +typedef void (*prc_Mod_t) (void *pdata, float v); // modulation function for processor + +#define OP_LEFT 0 // batch process left channel in place +#define OP_RIGHT 1 // batch process right channel in place +#define OP_LEFT_DUPLICATE 2 // batch process left channel in place, duplicate to right channel + +#define PRC_NULL 0 // pass through - must be 0 +#define PRC_DLY 1 // simple feedback reverb +#define PRC_RVA 2 // parallel reverbs +#define PRC_FLT 3 // lowpass or highpass filter +#define PRC_CRS 4 // chorus +#define PRC_PTC 5 // pitch shifter +#define PRC_ENV 6 // adsr envelope +#define PRC_LFO 7 // lfo +#define PRC_EFO 8 // envelope follower +#define PRC_MDY 9 // mod delay +#define PRC_DFR 10 // diffusor - n series allpass delays +#define PRC_AMP 11 // amplifier with distortion + +#define QUA_LO 0 // quality of filter or reverb. Must be 0,1,2,3. +#define QUA_MED 1 +#define QUA_HI 2 +#define QUA_VHI 3 +#define QUA_MAX QUA_VHI + +#define CPRCPARAMS 16 // up to 16 floating point params for each processor type + +// processor definition - one for each running instance of a dsp processor + +struct prc_t +{ + int type; // PRC type + + float prm[CPRCPARAMS]; // dsp processor parameters - array of floats + + prc_Param_t pfnParam; // allocation function - takes ptr to prc, returns ptr to specialized data struct for proc type + prc_GetNext_t pfnGetNext; // get next function + prc_GetNextN_t pfnGetNextN; // batch version of get next + prc_Free_t pfnFree; // free function + prc_Mod_t pfnMod; // modulation function + + void *pdata; // processor state data - ie: pdly, pflt etc. +}; + +// processor parameter ranges - for validating parameters during allocation of new processor + +typedef struct prm_rng_t +{ + int iprm; // parameter index + float lo; // min value of parameter + float hi; // max value of parameter +} prm_rng_s; + +void PRC_CheckParams ( prc_t *pprc, prm_rng_t *prng ); + +/////////// +// Filters +/////////// + +#define CFLTS 64 // max number of filters simultaneously active +#define FLT_M 12 // max order of any filter + +#define FLT_LP 0 // lowpass filter +#define FLT_HP 1 // highpass filter +#define FLT_BP 2 // bandpass filter +#define FTR_MAX FLT_BP + +// flt parameters + +struct flt_t +{ + bool fused; // true if slot in use + + int b[FLT_M+1]; // filter numerator parameters (convert 0.0-1.0 to 0-PMAX representation) + int a[FLT_M+1]; // filter denominator parameters (convert 0.0-1.0 to 0-PMAX representation) + int w[FLT_M+1]; // filter state - samples (dimension of max (M, L)) + int L; // filter order numerator (dimension of a[M+1]) + int M; // filter order denominator (dimension of b[L+1]) + int N; // # of series sections - 1 (0 = 1 section, 1 = 2 sections etc) + + flt_t *pf1; // series cascaded versions of filter + flt_t *pf2; + flt_t *pf3; +}; + +// flt flts + +flt_t flts[CFLTS]; + +void FLT_Init ( flt_t *pf ) { if ( pf ) Q_memset ( pf, 0, sizeof (flt_t) ); } +void FLT_InitAll ( void ) { for ( int i = 0 ; i < CFLTS; i++ ) FLT_Init ( &flts[i] ); } + +void FLT_Free ( flt_t *pf ) +{ + if ( pf ) + { + if (pf->pf1) + Q_memset ( pf->pf1, 0, sizeof (flt_t) ); + + if (pf->pf2) + Q_memset ( pf->pf2, 0, sizeof (flt_t) ); + + if (pf->pf3) + Q_memset ( pf->pf3, 0, sizeof (flt_t) ); + + Q_memset ( pf, 0, sizeof (flt_t) ); + } +} + +void FLT_FreeAll ( void ) { for (int i = 0 ; i < CFLTS; i++) FLT_Free ( &flts[i] ); } + + +// find a free filter from the filter pool +// initialize filter numerator, denominator b[0..M], a[0..L] +// gain scales filter numerator +// N is # of series sections - 1 + +flt_t * FLT_Alloc ( int N, int M, int L, int *a, int *b, float gain ) +{ + int i, j; + flt_t *pf = NULL; + + for (i = 0; i < CFLTS; i++) + { + if ( !flts[i].fused ) + { + pf = &flts[i]; + + // transfer filter params into filter struct + pf->M = M; + pf->L = L; + pf->N = N; + + for (j = 0; j <= M; j++) + pf->a[j] = a[j]; + + for (j = 0; j <= L; j++) + pf->b[j] = (int)((float)(b[j]) * gain); + + pf->pf1 = NULL; + pf->pf2 = NULL; + pf->pf3 = NULL; + + pf->fused = true; + break; + } + } + + Assert(pf); // make sure we're not trying to alloc more than CFLTS flts + + return pf; +} + +// convert filter params cutoff and type into +// iir transfer function params M, L, a[], b[] + +// iir filter, 1st order, transfer function is H(z) = b0 + b1 Z^-1 / a0 + a1 Z^-1 +// or H(z) = b0 - b1 Z^-1 / a0 + a1 Z^-1 for lowpass + +// design cutoff filter at 3db (.5 gain) p579 + +void FLT_Design_3db_IIR ( float cutoff, float ftype, int *pM, int *pL, int *a, int *b ) +{ + // ftype: FLT_LP, FLT_HP, FLT_BP + + double Wc = 2.0 * M_PI * cutoff / SOUND_DMA_SPEED; // radians per sample + double Oc; + double fa; + double fb; + + // calculations: + // Wc = 2pi * fc/44100 convert to radians + // Oc = tan (Wc/2) * Gc / sqt ( 1 - Gc^2) get analog version, low pass + // Oc = tan (Wc/2) * (sqt (1 - Gc^2)) / Gc analog version, high pass + // Gc = 10 ^ (-Ac/20) gain at cutoff. Ac = 3db, so Gc^2 = 0.5 + // a = ( 1 - Oc ) / ( 1 + Oc ) + // b = ( 1 - a ) / 2 + + Oc = tan ( Wc / 2.0 ); + + fa = ( 1.0 - Oc ) / ( 1.0 + Oc ); + + fb = ( 1.0 - fa ) / 2.0; + + if ( ftype == FLT_HP ) + fb = ( 1.0 + fa ) / 2.0; + + a[0] = 0; // a0 always ignored + a[1] = (int)( -fa * PMAX ); // quantize params down to 0-PMAX >> PBITS + b[0] = (int)( fb * PMAX ); + b[1] = b[0]; + + if ( ftype == FLT_HP ) + b[1] = -b[1]; + + *pM = *pL = 1; + + return; +} + +// filter parameter order + +typedef enum +{ + flt_iftype, + flt_icutoff, + flt_iqwidth, + flt_iquality, + flt_igain, + + flt_cparam // # of params +} flt_e; + +// filter parameter ranges + +prm_rng_t flt_rng[] = { + + {flt_cparam, 0, 0}, // first entry is # of parameters + + {flt_iftype, 0, FTR_MAX}, // filter type FLT_LP, FLT_HP, FLT_BP + {flt_icutoff, 10, 22050}, // cutoff frequency in hz at -3db gain + {flt_iqwidth, 0, 11025}, // width of BP (cut in starts at cutoff) + {flt_iquality, 0, QUA_MAX}, // QUA_LO, _MED, _HI, _VHI = # of series sections + {flt_igain, 0.0, 10.0}, // output gain 0-10.0 +}; + + +// convert prc float params to iir filter params, alloc filter and return ptr to it +// filter quality set by prc quality - 0,1,2 + +flt_t * FLT_Params ( prc_t *pprc ) +{ + float qual = pprc->prm[flt_iquality]; + float cutoff = pprc->prm[flt_icutoff]; + float ftype = pprc->prm[flt_iftype]; + float qwidth = pprc->prm[flt_iqwidth]; + float gain = pprc->prm[flt_igain]; + + int L = 0; // numerator order + int M = 0; // denominator order + int b[FLT_M+1]; // numerator params 0..PMAX + int b_scaled[FLT_M+1]; // gain scaled numerator + int a[FLT_M+1]; // denominator params 0..PMAX + + int L_bp = 0; // bandpass numerator order + int M_bp = 0; // bandpass denominator order + int b_bp[FLT_M+1]; // bandpass numerator params 0..PMAX + int b_bp_scaled[FLT_M+1]; // gain scaled numerator + int a_bp[FLT_M+1]; // bandpass denominator params 0..PMAX + + int N; // # of series sections + bool bpass = false; + + // if qwidth > 0 then alloc bandpass filter (pf is lowpass) + + if ( qwidth > 0.0 ) + bpass = true; + + if (bpass) + { + ftype = FLT_LP; + } + + // low pass and highpass filter design + + // 1st order IIR filter, 3db cutoff at fc + + if ( bpass ) + { + // highpass section + + FLT_Design_3db_IIR ( cutoff, FLT_HP, &M_bp, &L_bp, a_bp, b_bp ); + M_bp = clamp (M_bp, 1, FLT_M); + L_bp = clamp (L_bp, 1, FLT_M); + cutoff += qwidth; + } + + // lowpass section + + FLT_Design_3db_IIR ( cutoff, (int)ftype, &M, &L, a, b ); + + M = clamp (M, 1, FLT_M); + L = clamp (L, 1, FLT_M); + + // quality = # of series sections - 1 + + N = clamp ((int)qual, 0, 3); + + // make sure we alloc at least 2 filters + + if (bpass) + N = max(N, 1); + + flt_t *pf0 = NULL; + flt_t *pf1 = NULL; + flt_t *pf2 = NULL; + flt_t *pf3 = NULL; + + // scale b numerators with gain - only scale for first filter if series filters + + for (int i = 0; i < FLT_M; i++) + { + b_bp_scaled[i] = (int)((float)(b_bp[i]) * gain ); + b_scaled[i] = (int)((float)(b[i]) * gain ); + } + + if (bpass) + { + // 1st filter is lowpass + + pf0 = FLT_Alloc ( N, M_bp, L_bp, a_bp, b_bp_scaled, 1.0 ); + } + else + { + pf0 = FLT_Alloc ( N, M, L, a, b_scaled, 1.0 ); + } + + // allocate series filters + + if (pf0) + { + switch (N) + { + case 3: + // alloc last filter as lowpass also if FLT_BP + if (bpass) + pf3 = FLT_Alloc ( 0, M_bp, L_bp, a_bp, b_bp, 1.0 ); + else + pf3 = FLT_Alloc ( 0, M, L, a, b, 1.0 ); + case 2: + pf2 = FLT_Alloc ( 0, M, L, a, b, 1.0 ); + case 1: + pf1 = FLT_Alloc ( 0, M, L, a, b, 1.0 ); + case 0: + break; + } + + pf0->pf1 = pf1; + pf0->pf2 = pf2; + pf0->pf3 = pf3; + } + + return pf0; +} + +inline void * FLT_VParams ( void *p ) +{ + PRC_CheckParams( (prc_t *)p, flt_rng); + return (void *) FLT_Params ((prc_t *)p); +} + +inline void FLT_Mod ( void *p, float v ) { return; } + +// get next filter value for filter pf and input x + +inline int FLT_GetNext ( flt_t *pf, int x ) +{ + flt_t *pf1; + flt_t *pf2; + flt_t *pf3; + int y; + + switch( pf->N ) + { + default: + case 0: + return IIRFilter_Update_Order1(pf->a, pf->L, pf->b, pf->w, x); + case 1: + pf1 = pf->pf1; + + y = IIRFilter_Update_Order1(pf->a, pf->L, pf->b, pf->w, x); + return IIRFilter_Update_Order1(pf1->a, pf1->L, pf1->b, pf1->w, y); + case 2: + pf1 = pf->pf1; + pf2 = pf->pf2; + + y = IIRFilter_Update_Order1(pf->a, pf->L, pf->b, pf->w, x); + y = IIRFilter_Update_Order1(pf1->a, pf1->L, pf1->b, pf1->w, y); + return IIRFilter_Update_Order1(pf2->a, pf2->L, pf2->b, pf2->w, y); + case 3: + pf1 = pf->pf1; + pf2 = pf->pf2; + pf3 = pf->pf3; + + y = IIRFilter_Update_Order1(pf->a, pf->L, pf->b, pf->w, x); + y = IIRFilter_Update_Order1(pf1->a, pf1->L, pf1->b, pf1->w, y); + y = IIRFilter_Update_Order1(pf2->a, pf2->L, pf2->b, pf2->w, y); + return IIRFilter_Update_Order1(pf3->a, pf3->L, pf3->b, pf3->w, y); + } +} + +// batch version for performance + +inline void FLT_GetNextN( flt_t *pflt, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = FLT_GetNext( pflt, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = FLT_GetNext( pflt, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = FLT_GetNext( pflt, pb->left ); + pb++; + } + return; + } +} + +/////////////////////////////////////////////////////////////////////////// +// Positional updaters for pitch shift etc +/////////////////////////////////////////////////////////////////////////// + +// looping position within a wav, with integer and fractional parts +// used for pitch shifting, upsampling/downsampling +// 20 bits of fraction, 8+ bits of integer + +struct pos_t +{ + + fix20int step; // wave table whole and fractional step value + fix20int cstep; // current cummulative step value + int pos; // current position within wav table + + int D; // max dimension of array w[0...D] ie: # of samples = D+1 +}; + +// circular wrap of pointer p, relative to array w +// D max buffer index w[0...D] (count of samples in buffer is D+1) +// i circular index + +inline void POS_Wrap ( int D, int *i ) +{ + if ( *i > D ) + *i -= D + 1; // when *pi = D + 1, it wraps around to *pi = 0 + + if ( *i < 0 ) + *i += D + 1; // when *pi = - 1, it wraps around to *pi = D +} + +// set initial update value - fstep can have no more than 8 bits of integer and 20 bits of fract +// D is array max dimension w[0...D] (ie: size D+1) +// w is ptr to array +// p is ptr to pos_t to initialize + +inline void POS_Init( pos_t *p, int D, float fstep ) +{ + float step = fstep; + + // make sure int part of step is capped at fix20_intmax + + if ((int)step > FIX20_INTMAX) + step = (step - (int)step) + FIX20_INTMAX; + + p->step = FLOAT_TO_FIX20(step); // convert fstep to fixed point + p->cstep = 0; + p->pos = 0; // current update value + + p->D = D; // always init to end value, in case we're stepping backwards +} + +// change step value - this is an instantaneous change, not smoothed. + +inline void POS_ChangeVal( pos_t *p, float fstepnew ) +{ + p->step = FLOAT_TO_FIX20( fstepnew ); // convert fstep to fixed point +} + +// return current integer position, then update internal position value + +inline int POS_GetNext ( pos_t *p ) +{ + + //float f = FIX20_TO_FLOAT(p->cstep); + //int i1 = FIX20_INTPART(p->cstep); + //float f1 = FIX20_TO_FLOAT(FIX20_FRACPART(p->cstep)); + //float f2 = FIX20_TO_FLOAT(p->step); + + p->cstep += p->step; // update accumulated fraction step value (fixed point) + p->pos += FIX20_INTPART( p->cstep ); // update pos with integer part of accumulated step + p->cstep = FIX20_FRACPART( p->cstep ); // throw away the integer part of accumulated step + + // wrap pos around either end of buffer if needed + + POS_Wrap(p->D, &(p->pos)); + + // make sure returned position is within array bounds + + Assert (p->pos <= p->D); + + return p->pos; +} + +// oneshot position within wav +struct pos_one_t +{ + pos_t p; // pos_t + + bool fhitend; // flag indicating we hit end of oneshot wav +}; + +// set initial update value - fstep can have no more than 8 bits of integer and 20 bits of fract +// one shot position - play only once, don't wrap, when hit end of buffer, return last position + +inline void POS_ONE_Init( pos_one_t *p1, int D, float fstep ) +{ + POS_Init( &p1->p, D, fstep ) ; + + p1->fhitend = false; +} + +// return current integer position, then update internal position value + +inline int POS_ONE_GetNext ( pos_one_t *p1 ) +{ + int pos; + pos_t *p0; + + pos = p1->p.pos; // return current position + + if (p1->fhitend) + return pos; + + p0 = &(p1->p); + p0->cstep += p0->step; // update accumulated fraction step value (fixed point) + p0->pos += FIX20_INTPART( p0->cstep ); // update pos with integer part of accumulated step + //p0->cstep = SIGN(p0->cstep) * FIX20_FRACPART( p0->cstep ); + p0->cstep = FIX20_FRACPART( p0->cstep ); // throw away the integer part of accumulated step + + // if we wrapped, stop updating, always return last position + // if step value is 0, return hit end + + if (!p0->step || p0->pos < 0 || p0->pos >= p0->D ) + p1->fhitend = true; + else + pos = p0->pos; + + // make sure returned value is within array bounds + + Assert ( pos <= p0->D ); + + return pos; +} + + +///////////////////// +// Reverbs and delays +///////////////////// + +#define CDLYS 128 // max delay lines active. Also used for lfos. + +#define DLY_PLAIN 0 // single feedback loop +#define DLY_ALLPASS 1 // feedback and feedforward loop - flat frequency response (diffusor) +#define DLY_LOWPASS 2 // lowpass filter in feedback loop +#define DLY_LINEAR 3 // linear delay, no feedback, unity gain +#define DLY_FLINEAR 4 // linear delay with lowpass filter and output gain +#define DLY_LOWPASS_4TAP 5 // lowpass filter in feedback loop, 4 delay taps +#define DLY_PLAIN_4TAP 6 // single feedback loop, 4 delay taps + +#define DLY_MAX DLY_PLAIN_4TAP + +#define DLY_HAS_MULTITAP(a) ((a) == DLY_LOWPASS_4TAP || (a) == DLY_PLAIN_4TAP) +#define DLY_HAS_FILTER(a) ((a) == DLY_FLINEAR || (a) == DLY_LOWPASS || (a) == DLY_LOWPASS_4TAP) + +#define DLY_TAP_FEEDBACK_GAIN 0.25 // drop multitap feedback to compensate for sum of taps in dly_*multitap() + +#define DLY_NORMALIZING_REDUCTION_MAX 0.25 // don't reduce gain (due to feedback) below N% of original gain + +// delay line + +struct dly_t +{ + + bool fused; // true if dly is in use + int type; // delay type + + int D; // delay size, in samples + int t; // current tap, <= D + int tnew; // crossfading to tnew + int xf; // crossfade value of t (0..PMAX) + int t1,t2,t3; // additional taps for multi-tap delays + int a1,a2,a3; // feedback values for taps + int D0; // original delay size (only relevant if calling DLY_ChangeVal) + int *p; // circular buffer pointer + int *w; // array of samples + + int a; // feedback value 0..PMAX,normalized to 0-1.0 + int b; // gain value 0..PMAX, normalized to 0-1.0 + + flt_t *pflt; // pointer to filter, if type DLY_LOWPASS +}; + +dly_t dlys[CDLYS]; // delay lines + +void DLY_Init ( dly_t *pdly ) { if ( pdly ) Q_memset( pdly, 0, sizeof (dly_t)); } +void DLY_InitAll ( void ) { for (int i = 0 ; i < CDLYS; i++) DLY_Init ( &dlys[i] ); } +void DLY_Free ( dly_t *pdly ) +{ + // free memory buffer + + if ( pdly ) + { + FLT_Free ( pdly->pflt ); + + if ( pdly->w ) + { + delete[] pdly->w; + } + + // free dly slot + + Q_memset ( pdly, 0, sizeof (dly_t) ); + } +} + + +void DLY_FreeAll ( void ) { for (int i = 0; i < CDLYS; i++ ) DLY_Free ( &dlys[i] ); } + +// return adjusted feedback value for given dly +// such that decay time is same as that for dmin and fbmin + +// dmin - minimum delay +// fbmin - minimum feedback +// dly - delay to match decay to dmin, fbmin + +float DLY_NormalizeFeedback ( int dmin, float fbmin, int dly ) +{ + // minimum decay time T to -60db for a simple reverb is: + + // Tmin = (ln 10^-3 / Ln fbmin) * (Dmin / fs) + + // where fs = sample frequency + + // similarly, + + // Tdly = (ln 10^-3 / Ln fb) * (D / fs) + + // setting Tdly = Tmin and solving for fb gives: + + // D / Dmin = ln fb / ln fbmin + + // since y^x = z gives x = ln z / ln y + + // fb = fbmin ^ (D/Dmin) + + float fb = powf (fbmin, (float)dly / (float) dmin); + + return fb; +} + +// set up 'b' gain parameter of feedback delay to +// compensate for gain caused by feedback 'fb'. + +void DLY_SetNormalizingGain ( dly_t *pdly, int feedback ) +{ + // compute normalized gain, set as output gain + + // calculate gain of delay line with feedback, and use it to + // reduce output. ie: force delay line with feedback to unity gain + + // for constant input x with feedback fb: + + // out = x + x*fb + x * fb^2 + x * fb^3... + // gain = out/x + // so gain = 1 + fb + fb^2 + fb^3... + // which, by the miracle of geometric series, equates to 1/1-fb + // thus, gain = 1/(1-fb) + + float fgain = 0; + float gain; + int b; + float fb = (float)feedback; + + fb = fb / (float)PMAX; + fb = fpmin(fb, 0.999f); + + // if b is 0, set b to PMAX (1) + + b = pdly->b ? pdly->b : PMAX; + + fgain = 1.0 / (1.0 - fb); + + // compensating gain - multiply rva output by gain then >> PBITS + + gain = (int)((1.0 / fgain) * PMAX); + + gain = gain * 4; // compensate for fact that gain calculation is for +/- 32767 amplitude wavs + // ie: ok to allow a bit more gain because most wavs are not at theoretical peak amplitude at all times + + // limit gain reduction to N% PMAX + + gain = clamp (gain, (float)(PMAX * DLY_NORMALIZING_REDUCTION_MAX), (float)PMAX); + + gain = ((float)b/(float)PMAX) * gain; // scale final gain by pdly->b. + + pdly->b = (int)gain; +} + +void DLY_ChangeTaps ( dly_t *pdly, int t0, int t1, int t2, int t3 ); + +// allocate a new delay line +// D number of samples to delay +// a feedback value (0-PMAX normalized to 0.0-1.0) +// b gain value (0-PMAX normalized to 0.0-1.0) - this is folded into the filter fb params +// if DLY_LOWPASS or DLY_FLINEAR: +// L - numerator order of filter +// M - denominator order of filter +// fb - numerator params, M+1 +// fa - denominator params, L+1 + +dly_t * DLY_AllocLP ( int D, int a, int b, int type, int M, int L, int *fa, int *fb ) +{ + int *w; + int i; + dly_t *pdly = NULL; + int feedback; + + // find open slot + + for (i = 0; i < CDLYS; i++) + { + if (!dlys[i].fused) + { + pdly = &dlys[i]; + DLY_Init( pdly ); + break; + } + } + + if ( i == CDLYS ) + { + DevMsg ("DSP: Warning, failed to allocate delay line.\n" ); + return NULL; // all delay lines in use + } + + // save original feedback value + + feedback = a; + + // adjust feedback a, gain b if delay is multitap unit + + if ( DLY_HAS_MULTITAP(type) ) + { + // split output gain over 4 taps + + b = (int)((float)(b) * DLY_TAP_FEEDBACK_GAIN); + } + + if ( DLY_HAS_FILTER(type) ) + { + // alloc lowpass iir_filter + // delay feedback gain is built into filter gain + + float gain = (float)a / (float)(PMAX); + + pdly->pflt = FLT_Alloc( 0, M, L, fa, fb, gain ); + if ( !pdly->pflt ) + { + DevMsg ("DSP: Warning, failed to allocate filter for delay line.\n" ); + return NULL; + } + } + + // alloc delay memory + w = new int[D+1]; + if ( !w ) + { + Warning( "Sound DSP: Failed to lock.\n"); + FLT_Free ( pdly->pflt ); + return NULL; + } + + // clear delay array + + Q_memset (w, 0, sizeof(int) * (D+1)); + + // init values + + pdly->type = type; + pdly->D = D; + pdly->t = D; // set delay tap to full delay + pdly->tnew = D; + pdly->xf = 0; + pdly->D0 = D; + pdly->p = w; // init circular pointer to head of buffer + pdly->w = w; + pdly->a = min( a, PMAX - 1 ); // do not allow 100% feedback + pdly->b = b; + pdly->fused = true; + + if ( type == DLY_LINEAR || type == DLY_FLINEAR ) + { + // linear delay has no feedback and unity gain + + pdly->a = 0; + pdly->b = PMAX; + } + else + { + // adjust b to compensate for feedback gain of steady state max input + + DLY_SetNormalizingGain( pdly, feedback ); + } + + if ( DLY_HAS_MULTITAP(type) ) + { + // initially set up all taps to same value - caller uses DLY_ChangeTaps to change values + + DLY_ChangeTaps( pdly, D, D, D, D ); + } + + return (pdly); +} + +// allocate lowpass or allpass delay + +dly_t * DLY_Alloc( int D, int a, int b, int type ) +{ + return DLY_AllocLP( D, a, b, type, 0, 0, 0, 0 ); +} + + +// Allocate new delay, convert from float params in prc preset to internal parameters +// Uses filter params in prc if delay is type lowpass + +// delay parameter order + +typedef enum { + + dly_idtype, // NOTE: first 8 params must match those in mdy_e + dly_idelay, + dly_ifeedback, + dly_igain, + + dly_iftype, + dly_icutoff, + dly_iqwidth, + dly_iquality, + + dly_itap1, + dly_itap2, + dly_itap3, + + dly_cparam + +} dly_e; + + +// delay parameter ranges + +prm_rng_t dly_rng[] = { + + {dly_cparam, 0, 0}, // first entry is # of parameters + + // delay params + + {dly_idtype, 0, DLY_MAX}, // delay type DLY_PLAIN, DLY_LOWPASS, DLY_ALLPASS etc + {dly_idelay, -1.0, 1000.0}, // delay in milliseconds (-1 forces auto dsp to set delay value from room size) + {dly_ifeedback, 0.0, 0.99}, // feedback 0-1.0 + {dly_igain, 0.0, 10.0}, // final gain of output stage, 0-10.0 + + // filter params if dly type DLY_LOWPASS or DLY_FLINEAR + + {dly_iftype, 0, FTR_MAX}, + {dly_icutoff, 10.0, 22050.0}, + {dly_iqwidth, 100.0, 11025.0}, + {dly_iquality, 0, QUA_MAX}, + // note: -1 flag tells auto dsp to get value directly from room size + {dly_itap1, -1.0, 1000.0}, // delay in milliseconds NOTE: delay > tap3 > tap2 > tap1 + {dly_itap2, -1.0, 1000.0}, // delay in milliseconds + {dly_itap3, -1.0, 1000.0}, // delay in milliseconds +}; + +dly_t * DLY_Params ( prc_t *pprc ) +{ + dly_t *pdly = NULL; + int D, a, b; + + float delay = fabs(pprc->prm[dly_idelay]); + float feedback = pprc->prm[dly_ifeedback]; + float gain = pprc->prm[dly_igain]; + int type = pprc->prm[dly_idtype]; + + float ftype = pprc->prm[dly_iftype]; + float cutoff = pprc->prm[dly_icutoff]; + float qwidth = pprc->prm[dly_iqwidth]; + float qual = pprc->prm[dly_iquality]; + + float t1 = fabs(pprc->prm[dly_itap1]); + float t2 = fabs(pprc->prm[dly_itap2]); + float t3 = fabs(pprc->prm[dly_itap3]); + + D = MSEC_TO_SAMPS(delay); // delay samples + a = feedback * PMAX; // feedback + b = gain * PMAX; // gain + + switch ( (int) type ) + { + case DLY_PLAIN: + case DLY_PLAIN_4TAP: + case DLY_ALLPASS: + case DLY_LINEAR: + pdly = DLY_Alloc( D, a, b, type ); + break; + + case DLY_FLINEAR: + case DLY_LOWPASS: + case DLY_LOWPASS_4TAP: + { + // set up dummy lowpass filter to convert params + + prc_t prcf; + + prcf.prm[flt_iquality] = qual; // 0,1,2 - (0 or 1 low quality implies faster execution time) + prcf.prm[flt_icutoff] = cutoff; + prcf.prm[flt_iftype] = ftype; + prcf.prm[flt_iqwidth] = qwidth; + prcf.prm[flt_igain] = 1.0; + + flt_t *pflt = (flt_t *)FLT_Params ( &prcf ); + + if ( !pflt ) + { + DevMsg ("DSP: Warning, failed to allocate filter.\n" ); + return NULL; + } + + pdly = DLY_AllocLP ( D, a, b, type, pflt->M, pflt->L, pflt->a, pflt->b ); + + FLT_Free ( pflt ); + break; + } + } + + // set up multi-tap delays + + if ( pdly && DLY_HAS_MULTITAP((int)type) ) + DLY_ChangeTaps( pdly, D, MSEC_TO_SAMPS(t1), MSEC_TO_SAMPS(t2), MSEC_TO_SAMPS(t3) ); + + return pdly; +} + +inline void * DLY_VParams ( void *p ) +{ + PRC_CheckParams( (prc_t *)p, dly_rng ); + return (void *) DLY_Params ((prc_t *)p); +} + +// get next value from delay line, move x into delay line + +inline int DLY_GetNext ( dly_t *pdly, int x ) +{ + switch (pdly->type) + { + default: + case DLY_PLAIN: + return ReverbSimple( pdly->D, pdly->t, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_ALLPASS: + return DelayAllpass( pdly->D, pdly->t, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_LOWPASS: + return DelayLowpass( pdly->D, pdly->t, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + case DLY_LINEAR: + return DelayLinear( pdly->D, pdly->t, pdly->w, &pdly->p, x ); + case DLY_FLINEAR: + return DelayLinear_lowpass( pdly->D, pdly->t, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + case DLY_PLAIN_4TAP: + return ReverbSimple_multitap( pdly->D, pdly->t, pdly->t1, pdly->t2, pdly->t3, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_LOWPASS_4TAP: + return DelayLowpass_multitap( pdly->D, pdly->t, pdly->t1, pdly->t2,pdly->t3, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + } +} + +inline int DLY_GetNextXfade ( dly_t *pdly, int x ) +{ + + switch (pdly->type) + { + default: + case DLY_PLAIN: + return ReverbSimple_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_ALLPASS: + return DelayAllpass_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_LOWPASS: + return DelayLowpass_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + case DLY_LINEAR: + return DelayLinear_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->w, &pdly->p, x ); + case DLY_FLINEAR: + return DelayLinear_lowpass_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + case DLY_PLAIN_4TAP: + return ReverbSimple_multitap_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->t1, pdly->t2, pdly->t3, pdly->w, &pdly->p, pdly->a, pdly->b, x ); + case DLY_LOWPASS_4TAP: + return DelayLowpass_multitap_xfade( pdly->D, pdly->t, pdly->tnew, pdly->xf, pdly->t1, pdly->t2, pdly->t3, pdly->w, &(pdly->p), pdly->a, pdly->b, pdly->pflt->a, pdly->pflt->L, pdly->pflt->b, pdly->pflt->w, x ); + } +} + +// batch version for performance +// UNDONE: a) unwind this more - pb increments by 2 to avoid pb->left or pb->right deref. +// UNDONE: b) all filter and delay params are dereferenced outside of DLY_GetNext and passed as register values +// UNDONE: c) pull case statement in dly_getnext out, so loop directly calls the inline dly_*() routine. + +inline void DLY_GetNextN( dly_t *pdly, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = DLY_GetNext( pdly, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = DLY_GetNext( pdly, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = DLY_GetNext( pdly, pb->left ); + pb++; + } + return; + } +} + +// get tap on t'th sample in delay - don't update buffer pointers, this is done via DLY_GetNext +// Only valid for DLY_LINEAR. + +inline int DLY_GetTap ( dly_t *pdly, int t ) +{ + return GetDly (pdly->D, pdly->w, pdly->p, t ); +} + +#define SWAP(a,b,t) {(t) = (a); (a) = (b); (b) = (t);} + +// make instantaneous change to tap values t0..t3 +// all values of t must be less than original delay D +// only processed for DLY_LOWPASS_4TAP & DLY_PLAIN_4TAP +// NOTE: pdly->a feedback must have been set before this call! +void DLY_ChangeTaps ( dly_t *pdly, int t0, int t1, int t2, int t3 ) +{ + if (!pdly) + return; + + int temp; + + // sort taps to make sure t3 > t2 > t1 > t0 ! + + for (int i = 0; i < 4; i++) + { + if (t0 > t1) SWAP(t0, t1, temp); + if (t1 > t2) SWAP(t1, t2, temp); + if (t2 > t3) SWAP(t2, t3, temp); + } + + pdly->t = min ( t0, pdly->D0 ); + pdly->t1 = min ( t1, pdly->D0 ); + pdly->t2 = min ( t2, pdly->D0 ); + pdly->t3 = min ( t3, pdly->D0 ); + +} + +// make instantaneous change for first delay tap 't' to new delay value. +// t tap value must be <= original D (ie: we don't do any reallocation here) + +void DLY_ChangeVal ( dly_t *pdly, int t ) +{ + // never set delay > original delay + + pdly->t = min ( t, pdly->D0 ); +} + +// ignored - use MDY_ for modulatable delay + +inline void DLY_Mod ( void *p, float v ) { return; } + + +///////////////////////////////////////////////////////////////////////////// +// Ramp - used for varying smoothly between int parameters ie: modulation delays +///////////////////////////////////////////////////////////////////////////// + + +struct rmp_t +{ + int initval; // initial ramp value + int target; // final ramp value + int sign; // increasing (1) or decreasing (-1) ramp + + int yprev; // previous output value + bool fhitend; // true if hit end of ramp + bool bEndAtTime; // if true, fhitend is true when ramp time is hit (even if target not hit) + // if false, then fhitend is true only when target is hit + pos_one_t ps; // current ramp output +}; + +// ramp smoothly between initial value and target value in approx 'ramptime' seconds. +// (initial value may be greater or less than target value) +// never changes output by more than +1 or -1 (which can cause the ramp to take longer to complete than ramptime - see bEndAtTime) +// called once per sample while ramping +// ramptime - duration of ramp in seconds +// initval - initial ramp value +// targetval - target ramp value +// if bEndAtTime is true, then RMP_HitEnd returns true when ramp time is reached, EVEN IF TARGETVAL IS NOT REACHED +// if bEndAtTime is false, then RMP_HitEnd returns true when targetval is reached, EVEN IF DELTA IN RAMP VALUES IS > +/- 1 + +void RMP_Init( rmp_t *prmp, float ramptime, int initval, int targetval, bool bEndAtTime ) +{ + int rise; + int run; + + if (prmp) + Q_memset( prmp, 0, sizeof (rmp_t) ); + else + return; + + run = (int) (ramptime * SOUND_DMA_SPEED); // 'samples' in ramp + rise = (targetval - initval); // height of ramp + + // init fixed point iterator to iterate along the height of the ramp 'rise' + // always iterates from 0..'rise', increasing in value + + POS_ONE_Init( &prmp->ps, ABS( rise ), ABS((float) rise) / ((float) run) ); + + prmp->yprev = initval; + prmp->initval = initval; + prmp->target = targetval; + prmp->sign = SIGN( rise ); + prmp->bEndAtTime = bEndAtTime; + +} + +// continues from current position to new target position + +void RMP_SetNext( rmp_t *prmp, float ramptime, int targetval ) +{ + RMP_Init ( prmp, ramptime, prmp->yprev, targetval, prmp->bEndAtTime ); +} + +inline bool RMP_HitEnd ( rmp_t *prmp ) +{ + return prmp->fhitend; +} + +inline void RMP_SetEnd ( rmp_t *prmp ) +{ + prmp->fhitend = true; +} + +// get next ramp value & update ramp, if bEndAtTime is true, never varies by more than +1 or -1 between calls +// when ramp hits target value, it thereafter always returns last value + +inline int RMP_GetNext( rmp_t *prmp ) +{ + int y; + int d; + + // if we hit ramp end, return last value + + if (prmp->fhitend) + return prmp->yprev; + + // get next integer position in ramp height. + + d = POS_ONE_GetNext( &prmp->ps ); + + if ( prmp->ps.fhitend ) + prmp->fhitend = true; + + // increase or decrease from initval, depending on ramp sign + + if ( prmp->sign > 0 ) + y = prmp->initval + d; + else + y = prmp->initval - d; + + // if bEndAtTime is true, only update current height by a max of +1 or -1 + // this also means that for short ramp times, we may not hit target + + if (prmp->bEndAtTime) + { + if ( ABS( y - prmp->yprev ) >= 1 ) + prmp->yprev += prmp->sign; + } + else + { + // always hits target - but varies by more than +/- 1 + + prmp->yprev = y; + } + + return prmp->yprev; +} + +// get current ramp value, don't update ramp + +inline int RMP_GetCurrent( rmp_t *prmp ) +{ + return prmp->yprev; +} + + +////////////// +// mod delay +////////////// + +// modulate delay time anywhere from 0..D using MDY_ChangeVal. no output glitches (uses RMP) + +#define CMDYS 64 // max # of mod delays active (steals from delays) + +struct mdy_t +{ + bool fused; + + bool fchanging; // true if modulating to new delay value + + dly_t *pdly; // delay + + float ramptime; // ramp 'glide' time - time in seconds to change between values + + int mtime; // time in samples between delay changes. 0 implies no self-modulating + int mtimecur; // current time in samples until next delay change + float depth; // modulate delay from D to D - (D*depth) depth 0-1.0 + + int mix; // PMAX as % processed fx signal mix + + rmp_t rmp_interp; // interpolation ramp 0...PMAX + + bool bPhaseInvert; // if true, invert phase of output + +}; + +mdy_t mdys[CMDYS]; + +void MDY_Init( mdy_t *pmdy ) { if (pmdy) Q_memset( pmdy, 0, sizeof (mdy_t) ); }; +void MDY_Free( mdy_t *pmdy ) { if (pmdy) { DLY_Free (pmdy->pdly); Q_memset( pmdy, 0, sizeof (mdy_t) ); } }; +void MDY_InitAll() { for (int i = 0; i < CMDYS; i++) MDY_Init( &mdys[i] ); }; +void MDY_FreeAll() { for (int i = 0; i < CMDYS; i++) MDY_Free( &mdys[i] ); }; + + +// allocate mod delay, given previously allocated dly (NOTE: mod delay only sweeps tap 0, not t1,t2 or t3) +// ramptime is time in seconds for delay to change from dcur to dnew +// modtime is time in seconds between modulations. 0 if no self-modulation +// depth is 0-1.0 multiplier, new delay values when modulating are Dnew = randomlong (D - D*depth, D) +// mix - 0-1.0, default 1.0 for 100% fx mix - pans between input signal and fx signal + +mdy_t *MDY_Alloc ( dly_t *pdly, float ramptime, float modtime, float depth, float mix ) +{ + int i; + mdy_t *pmdy; + + if ( !pdly ) + return NULL; + + for (i = 0; i < CMDYS; i++) + { + if ( !mdys[i].fused ) + { + pmdy = &mdys[i]; + + MDY_Init ( pmdy ); + + pmdy->pdly = pdly; + + if ( !pmdy->pdly ) + { + DevMsg ("DSP: Warning, failed to allocate delay for mod delay.\n" ); + return NULL; + } + + pmdy->fused = true; + pmdy->ramptime = ramptime; + pmdy->mtime = SEC_TO_SAMPS(modtime); + pmdy->mtimecur = pmdy->mtime; + pmdy->depth = depth; + pmdy->mix = int ( PMAX * mix ); + pmdy->bPhaseInvert = false; + + return pmdy; + } + } + + DevMsg ("DSP: Warning, failed to allocate mod delay.\n" ); + return NULL; +} + +// change to new delay tap value t samples, ramp linearly over ramptime seconds + +void MDY_ChangeVal ( mdy_t *pmdy, int t ) +{ + // if D > original delay value, cap at original value + + t = min (pmdy->pdly->D0, t); + + pmdy->fchanging = true; + + // init interpolation ramp - always hit target + + RMP_Init ( &pmdy->rmp_interp, pmdy->ramptime, 0, PMAX, false ); + + // init delay xfade values + + pmdy->pdly->tnew = t; + pmdy->pdly->xf = 0; +} + +// interpolate between current and target delay values + +inline int MDY_GetNext( mdy_t *pmdy, int x ) +{ + int xout; + + if ( !pmdy->fchanging ) + { + // not modulating... + + xout = DLY_GetNext( pmdy->pdly, x ); + + if ( !pmdy->mtime ) + { + // return right away if not modulating (not changing and not self modulating) + + goto mdy_return; + } + } + else + { + // modulating... + + xout = DLY_GetNextXfade( pmdy->pdly, x ); + + // get xfade ramp & set up delay xfade value for next call to DLY_GetNextXfade() + + pmdy->pdly->xf = RMP_GetNext( &pmdy->rmp_interp ); // 0...PMAX + + if ( RMP_HitEnd( &pmdy->rmp_interp ) ) + { + // done. set delay tap & value = target + + DLY_ChangeVal( pmdy->pdly, pmdy->pdly->tnew ); + + pmdy->pdly->t = pmdy->pdly->tnew; + + pmdy->fchanging = false; + } + } + + // if self-modulating and timer has expired, get next change + + if ( pmdy->mtime && !pmdy->mtimecur-- ) + { + pmdy->mtimecur = pmdy->mtime; + + int D0 = pmdy->pdly->D0; + int Dnew; + float D1; + + // modulate between 0 and 100% of d0 + + D1 = (float)D0 * (1.0 - pmdy->depth); + + Dnew = RandomInt( (int)D1, D0 ); + + // set up modulation to new value + + MDY_ChangeVal ( pmdy, Dnew ); + } + +mdy_return: + + // reverse phase of output + + if ( pmdy->bPhaseInvert ) + xout = -xout; + + // 100% fx mix + + if ( pmdy->mix == PMAX) + return xout; + + // special case 50/50 mix + + if ( pmdy->mix == PMAX / 2) + return ( (xout + x) >> 1 ); + + // return mix of input and processed signal + + return ( x + (((xout - x) * pmdy->mix) >> PBITS) ); +} + + +// batch version for performance +// UNDONE: unwind MDY_GetNext so that it directly calls DLY_GetNextN: +// UNDONE: a) if not currently modulating and never self-modulating, then just unwind like DLY_GetNext +// UNDONE: b) if not currently modulating, figure out how many samples N until self-modulation timer kicks in again +// and stream out N samples just like DLY_GetNext + +inline void MDY_GetNextN( mdy_t *pmdy, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = MDY_GetNext( pmdy, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = MDY_GetNext( pmdy, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = MDY_GetNext( pmdy, pb->left ); + pb++; + } + return; + } +} + +// parameter order + +typedef enum { + + mdy_idtype, // NOTE: first 8 params must match params in dly_e + mdy_idelay, + mdy_ifeedback, + mdy_igain, + + mdy_iftype, + mdy_icutoff, + mdy_iqwidth, + mdy_iquality, + + mdy_imodrate, + mdy_imoddepth, + mdy_imodglide, + + mdy_imix, + mdy_ibxfade, + + mdy_cparam + +} mdy_e; + + +// parameter ranges + +prm_rng_t mdy_rng[] = { + + {mdy_cparam, 0, 0}, // first entry is # of parameters + + // delay params + + {mdy_idtype, 0, DLY_MAX}, // delay type DLY_PLAIN, DLY_LOWPASS, DLY_ALLPASS + {mdy_idelay, 0.0, 1000.0}, // delay in milliseconds + {mdy_ifeedback, 0.0, 0.99}, // feedback 0-1.0 + {mdy_igain, 0.0, 1.0}, // final gain of output stage, 0-1.0 + + // filter params if mdy type DLY_LOWPASS + + {mdy_iftype, 0, FTR_MAX}, + {mdy_icutoff, 10.0, 22050.0}, + {mdy_iqwidth, 100.0, 11025.0}, + {mdy_iquality, 0, QUA_MAX}, + + {mdy_imodrate, 0.01, 200.0}, // frequency at which delay values change to new random value. 0 is no self-modulation + {mdy_imoddepth, 0.0, 1.0}, // how much delay changes (decreases) from current value (0-1.0) + {mdy_imodglide, 0.01, 100.0}, // glide time between dcur and dnew in milliseconds + {mdy_imix, 0.0, 1.0} // 1.0 = full fx mix, 0.5 = 50% fx, 50% dry +}; + + +// convert user parameters to internal parameters, allocate and return + +mdy_t * MDY_Params ( prc_t *pprc ) +{ + mdy_t *pmdy; + dly_t *pdly; + + float ramptime = pprc->prm[mdy_imodglide] / 1000.0; // get ramp time in seconds + float modtime = 0.0f; + if ( pprc->prm[mdy_imodrate] != 0.0f ) + { + modtime = 1.0 / pprc->prm[mdy_imodrate]; // time between modulations in seconds + } + float depth = pprc->prm[mdy_imoddepth]; // depth of modulations 0-1.0 + float mix = pprc->prm[mdy_imix]; + + // alloc plain, allpass or lowpass delay + + pdly = DLY_Params( pprc ); + + if ( !pdly ) + return NULL; + + pmdy = MDY_Alloc ( pdly, ramptime, modtime, depth, mix ); + + return pmdy; +} + +inline void * MDY_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, mdy_rng ); + return (void *) MDY_Params ((prc_t *)p); +} + +// v is +/- 0-1.0 +// change current delay value 0..D + +void MDY_Mod ( mdy_t *pmdy, float v ) +{ + + int D0 = pmdy->pdly->D0; // base delay value + float v2; + + // if v is < -2.0 then delay is v + 10.0 + // invert phase of output. hack. + + if ( v < -2.0 ) + { + v = v + 10.0; + pmdy->bPhaseInvert = true; + } + else + { + pmdy->bPhaseInvert = false; + } + + v2 = -(v + 1.0)/2.0; // v2 varies -1.0-0.0 + + // D0 varies 0..D0 + + D0 = D0 + (int)((float)D0 * v2); + + // change delay + + MDY_ChangeVal( pmdy, D0 ); + + return; +} + + +/////////////////// +// Parallel reverbs +/////////////////// + +// Reverb A +// M parallel reverbs, mixed to mono output + +#define CRVAS 64 // max number of parallel series reverbs active + +#define CRVA_DLYS 12 // max number of delays making up reverb_a + +struct rva_t +{ + bool fused; + int m; // number of parallel plain or lowpass delays + int fparallel; // true if filters in parallel with delays, otherwise single output filter + flt_t *pflt; // series filters + + dly_t *pdlys[CRVA_DLYS]; // array of pointers to delays + mdy_t *pmdlys[CRVA_DLYS]; // array of pointers to mod delays + + bool fmoddly; // true if using mod delays +}; + +rva_t rvas[CRVAS]; + +void RVA_Init ( rva_t *prva ) { if ( prva ) Q_memset (prva, 0, sizeof (rva_t)); } +void RVA_InitAll( void ) { for (int i = 0; i < CRVAS; i++) RVA_Init ( &rvas[i] ); } + +// free parallel series reverb + +void RVA_Free( rva_t *prva ) +{ + int i; + + if ( prva ) + { + // free all delays + for (i = 0; i < CRVA_DLYS; i++) + DLY_Free ( prva->pdlys[i] ); + + // zero all ptrs to delays in mdy array + for (i = 0; i < CRVA_DLYS; i++) + { + if ( prva->pmdlys[i] ) + prva->pmdlys[i]->pdly = NULL; + } + + // free all mod delays + for (i = 0; i < CRVA_DLYS; i++) + MDY_Free ( prva->pmdlys[i] ); + + FLT_Free( prva->pflt ); + + Q_memset( prva, 0, sizeof (rva_t) ); + } +} + + +void RVA_FreeAll( void ) { for (int i = 0; i < CRVAS; i++) RVA_Free( &rvas[i] ); } + +// create parallel reverb - m parallel reverbs summed + +// D array of CRVB_DLYS reverb delay sizes max sample index w[0...D] (ie: D+1 samples) +// a array of reverb feedback parms for parallel reverbs (CRVB_P_DLYS) +// if a[i] < 0 then this is a predelay - use DLY_FLINEAR instead of DLY_LOWPASS +// b array of CRVB_P_DLYS - mix params for parallel reverbs +// m - number of parallel delays +// pflt - filter template, to be used by all parallel delays +// fparallel - true if filter operates in parallel with delays, otherwise filter output only +// fmoddly - > 0 if delays are all mod delays (milliseconds of delay modulation) +// fmodrate - # of delay repetitions between changes to mod delay +// ftaps - if > 0, use 4 taps per reverb delay unit (increases density) tap = D - n*ftaps n = 0,1,2,3 + +rva_t * RVA_Alloc ( int *D, int *a, int *b, int m, flt_t *pflt, int fparallel, float fmoddly, float fmodrate, float ftaps ) +{ + + int i; + int dtype; + rva_t *prva; + flt_t *pflt2 = NULL; + + bool btaps = ftaps > 0.0; + + // find open slot + + for ( i = 0; i < CRVAS; i++ ) + { + if ( !rvas[i].fused ) + break; + } + + // return null if no free slots + + if (i == CRVAS) + { + DevMsg ("DSP: Warning, failed to allocate reverb.\n" ); + return NULL; + } + + prva = &rvas[i]; + + // if series filter specified, alloc two series filters + + if ( pflt && !fparallel) + { + // use filter data as template for a filter on output (2 cascaded filters) + + pflt2 = FLT_Alloc (0, pflt->M, pflt->L, pflt->a, pflt->b, 1.0); + + if (!pflt2) + { + DevMsg ("DSP: Warning, failed to allocate flt for reverb.\n" ); + return NULL; + } + + pflt2->pf1 = FLT_Alloc (0, pflt->M, pflt->L, pflt->a, pflt->b, 1.0); + pflt2->N = 1; + } + + // allocate parallel delays + + for (i = 0; i < m; i++) + { + // set delay type + + if ( pflt && fparallel ) + // if a[i] param is < 0, allocate delay as predelay instead of feedback delay + dtype = a[i] < 0 ? DLY_FLINEAR : DLY_LOWPASS; + else + // if no filter specified, alloc as plain or multitap plain delay + dtype = btaps ? DLY_PLAIN_4TAP : DLY_PLAIN; + + if ( dtype == DLY_LOWPASS && btaps ) + dtype = DLY_LOWPASS_4TAP; + + // if filter specified and parallel specified, alloc 1 filter per delay + + if ( DLY_HAS_FILTER(dtype) ) + prva->pdlys[i] = DLY_AllocLP( D[i], abs(a[i]), b[i], dtype, pflt->M, pflt->L, pflt->a, pflt->b ); + else + prva->pdlys[i] = DLY_Alloc( D[i], abs(a[i]), b[i], dtype ); + + if ( DLY_HAS_MULTITAP(dtype) ) + { + // set up delay taps to increase density around delay value. + + // value of ftaps is the seed for all tap values + + float t1 = max((double)MSEC_TO_SAMPS(5), D[i] * (1.0 - ftaps * 3.141592) ); + float t2 = max((double)MSEC_TO_SAMPS(7), D[i] * (1.0 - ftaps * 1.697043) ); + float t3 = max((double)MSEC_TO_SAMPS(10), D[i] * (1.0 - ftaps * 0.96325) ); + + DLY_ChangeTaps( prva->pdlys[i], (int)t1, (int)t2, (int)t3, D[i] ); + } + } + + + if ( fmoddly > 0.0 ) + { + // alloc mod delays, using previously alloc'd delays + + // ramptime is time in seconds for delay to change from dcur to dnew + // modtime is time in seconds between modulations. 0 if no self-modulation + // depth is 0-1.0 multiplier, new delay values when modulating are Dnew = randomlong (D - D*depth, D) + + float ramptime; + float modtime; + float depth; + + for (i = 0; i < m; i++) + { + int Do = prva->pdlys[i]->D; + + modtime = (float)Do / (float)(SOUND_DMA_SPEED); // seconds per delay + depth = (fmoddly * 0.001f) / modtime; // convert milliseconds to 'depth' % + depth = clamp (depth, 0.01f, 0.99f); + modtime = modtime * fmodrate; // modulate every N delay passes + + ramptime = fpmin(20.0f/1000.0f, modtime / 2); // ramp between delay values in N ms + + prva->pmdlys[i] = MDY_Alloc( prva->pdlys[i], ramptime, modtime, depth, 1.0 ); + } + + prva->fmoddly = true; + } + + // if we failed to alloc any reverb, free all, return NULL + + for (i = 0; i < m; i++) + { + if ( !prva->pdlys[i] ) + { + FLT_Free( pflt2 ); + RVA_Free( prva ); + DevMsg ("DSP: Warning, failed to allocate delay for reverb.\n" ); + return NULL; + } + } + + prva->fused = true; + prva->m = m; + prva->fparallel = fparallel; + prva->pflt = pflt2; + return prva; +} + + +// parallel reverberator +// +// for each input sample x do: +// x0 = plain(D0,w0,&p0,a0,x) +// x1 = plain(D1,w1,&p1,a1,x) +// x2 = plain(D2,w2,&p2,a2,x) +// x3 = plain(D3,w3,&p3,a3,x) +// y = b0*x0 + b1*x1 + b2*x2 + b3*x3 +// +// rgdly - array of M delays: +// D - Delay values (typical - 29, 37, 44, 50, 27, 31) +// w - array of delayed values +// p - array of pointers to circular delay line pointers +// a - array of M feedback values (typical - all equal, like 0.75 * PMAX) +// b - array of M gain values for plain reverb outputs (1, .9, .8, .7) +// xin - input value +// if fparallel, filters are built into delays, +// otherwise, filter is in feedback loop + + +int g_MapIntoPBITSDivInt[] = +{ + 0, PMAX/1, PMAX/2, PMAX/3, PMAX/4, PMAX/5, PMAX/6, PMAX/7, PMAX/8, + PMAX/9, PMAX/10, PMAX/11,PMAX/12,PMAX/13,PMAX/14,PMAX/15,PMAX/16, +}; + +inline int RVA_GetNext( rva_t *prva, int x ) +{ + int m = prva->m; + int y = 0; + + if ( prva->fmoddly ) + { + // get output of parallel mod delays + + for (int i = 0; i < m; i++ ) + y += MDY_GetNext( prva->pmdlys[i], x ); + } + else + { + // get output of parallel delays + + for (int i = 0; i < m; i++ ) + y += DLY_GetNext( prva->pdlys[i], x ); + } + + // PERFORMANCE: y/m is now baked into the 'b' gain params for each delay ( b = b/m ) + // y = (y * g_MapIntoPBITSDivInt[m]) >> PBITS; + + if ( prva->fparallel ) + return y; + + // run series filters if present + + if ( prva->pflt ) + { + y = FLT_GetNext( prva->pflt, y); + } + + return y; +} + + +// batch version for performance +// UNDONE: unwind RVA_GetNextN so that it directly calls DLY_GetNextN or MDY_GetNextN + +inline void RVA_GetNextN( rva_t *prva, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = RVA_GetNext( prva, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = RVA_GetNext( prva, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = RVA_GetNext( prva, pb->left ); + pb++; + } + return; + } +} + +// reverb parameter order + +typedef enum +{ + +// parameter order + + rva_size_max, + rva_size_min, + + rva_inumdelays, + rva_ifeedback, + rva_igain, + + rva_icutoff, + + rva_ifparallel, + rva_imoddly, + rva_imodrate, + + rva_width, + rva_depth, + rva_height, + + rva_fbwidth, + rva_fbdepth, + rva_fbheight, + + rva_iftaps, + + rva_cparam // # of params +} rva_e; + +// filter parameter ranges + +prm_rng_t rva_rng[] = { + + {rva_cparam, 0, 0}, // first entry is # of parameters + + // reverb params + {rva_size_max, 0.0, 1000.0}, // max room delay in milliseconds + {rva_size_min, 0.0, 1000.0}, // min room delay in milliseconds + {rva_inumdelays,1.0, 12.0}, // controls # of parallel or series delays + {rva_ifeedback, 0.0, 1.0}, // feedback of delays + {rva_igain, 0.0, 10.0}, // output gain + + // filter params for each parallel reverb (quality set to 0 for max execution speed) + + {rva_icutoff, 10, 22050}, + + {rva_ifparallel, 0, 1}, // if 1, then all filters operate in parallel with delays. otherwise filter output only + {rva_imoddly, 0.0, 50.0}, // if > 0 then all delays are modulating delays, mod param controls milliseconds of mod depth + {rva_imodrate, 0.0, 10.0}, // how many delay repetitions pass between mod changes to delayl + + // override params - for more detailed description of room + // note: width/depth/height < 0 only for some automatic dsp presets + {rva_width, -1000.0, 1000.0}, // 0-1000.0 millisec (room width in feet) - used instead of size if non-zero + {rva_depth, -1000.0, 1000.0}, // 0-1000.0 room depth in feet - used instead of size if non-zero + {rva_height, -1000.0, 1000.0}, // 0-1000.0 room height in feet - used instead of size if non-zero + + {rva_fbwidth, -1.0, 1.0}, // 0-1.0 material reflectivity - used as feedback param instead of decay if non-zero + {rva_fbdepth, -1.0, 1.0}, // 0-1.0 material reflectivity - used as feedback param instead of decay if non-zero + {rva_fbheight, -1.0, 1.0}, // 0-1.0 material reflectivity - used as feedback param instead of decay if non-zero + // if < 0, a predelay is allocated, then feedback is -1*param given + + {rva_iftaps, 0.0, 0.333} // if > 0, use 3 extra taps with delay values = d * (1 - faps*n) n = 0,1,2,3 +}; + +#define RVA_BASEM 1 // base number of parallel delays + +// nominal delay and feedback values. More delays = more density. + +#define RVADLYSMAX 49 +float rvadlys[] = {18, 23, 28, 33, 42, 21, 26, 36, 39, 45, 47, 30}; +float rvafbs[] = {0.9, 0.9, 0.9, 0.85, 0.8, 0.9, 0.9, 0.85, 0.8, 0.8, 0.8, 0.85}; + +#define SWAP(a,b,t) {(t) = (a); (a) = (b); (b) = (t);} + +#define RVA_MIN_SEPARATION 7 // minimum separation between reverbs, in ms. + +// Construct D,a,b delay arrays given array of length,width,height sizes and feedback values +// rgd[] array of delay values in milliseconds (feet) +// rgf[] array of feedback values 0..1 +// m # of parallel reverbs to construct +// D[] array of output delay values for parallel reverbs +// a[] array of output feedback values +// b[] array of output gain values = 1/m +// gain - output gain +// feedback - default feedback if rgf members are 0 + +void RVA_ConstructDelays( float *rgd, float *rgf, int m, int *D, int *a, int *b, float gain, float feedback ) +{ + + int i; + float r; + int d; + float t, d1, d2, dm; + bool bpredelay; + + // sort descending, so rgd[0] is largest delay & rgd[2] is smallest + + if (rgd[2] > rgd[1]) { SWAP(rgd[2], rgd[1], t); SWAP(rgf[2], rgf[1], t); } + if (rgd[1] > rgd[0]) { SWAP(rgd[0], rgd[1], t); SWAP(rgf[0], rgf[1], t); } + if (rgd[2] > rgd[1]) { SWAP(rgd[2], rgd[1], t); SWAP(rgf[2], rgf[1], t); } + + // if all feedback values 0, use default feedback + + if (rgf[0] == 0.0 && rgf[1] == 0.0 && rgf[2] == 0.0 ) + { + // use feedback param for all + + rgf[0] = rgf[1] = rgf[2] = feedback; + + // adjust feedback down for larger delays so that decay is constant for all delays + + rgf[0] = DLY_NormalizeFeedback( rgd[2], rgf[2], rgd[0] ); + rgf[1] = DLY_NormalizeFeedback( rgd[2], rgf[2], rgd[1] ); + + } + + // make sure all reverbs are different by at least RVA_MIN_SEPARATION * m/3 m is 3,6,9 or 12 + + int dmin = (m/3) * RVA_MIN_SEPARATION; + + d1 = rgd[1] - rgd[2]; + + if (d1 <= dmin) + rgd[1] += (dmin-d1); // make difference = dmin + + d2 = rgd[0] - rgd[1]; + + if (d2 <= dmin) + rgd[0] += (dmin-d1); // make difference = dmin + + for ( i = 0; i < m; i++ ) + { + // reverberations due to room width, depth, height + // assume sound moves at approx 1ft/ms + + int j = (int)(fmod ((float)i, 3.0f)); // j counts 0,1,2 0,1,2 0,1.. + + d = (int)rgd[j]; + r = fabs(rgf[j]); + + bpredelay = ((rgf[j] < 0) && i < 3); + + // re-use predelay values as reverb values: + + if (rgf[j] < 0 && !bpredelay) + d = max((int)(rgd[j] / 4.0), RVA_MIN_SEPARATION); + + if (i < 3) + dm = 0.0; + else + dm = max( (double)(RVA_MIN_SEPARATION * (i/3)), ((i/3) * ((float)d * 0.18)) ); + + d += (int)dm; + D[i] = MSEC_TO_SAMPS(d); + + // D[i] = MSEC_TO_SAMPS(d + ((i/3) * RVA_MIN_SEPARATION)); // (i/3) counts 0,0,0 1,1,1 2,2,2 ... separate all reverbs by 5ms + + // feedback - due to wall/floor/ceiling reflectivity + a[i] = (int) min (0.999 * PMAX, (double)PMAX * r); + + if (bpredelay) + a[i] = -a[i]; // flag delay as predelay + + b[i] = (int)((float)(gain * PMAX) / (float)m); + } +} + +void RVA_PerfTest() +{ + double time1, time2; + + int i; + int k; + int j; + int m; + int a[100]; + + time1 = Plat_FloatTime(); + + for (m = 0; m < 1000; m++) + { + for (i = 0, j = 10000; i < 10000; i++, j--) + { + // j = j % 6; + // k = (i * j) >> PBITS; + + k = i / ((j % 6) + 1); + } + } + + time2 = Plat_FloatTime(); + + DevMsg("divide = %2.5f \n", (time2-time1)); + + + for (i=1;i<10;i++) + a[i] = PMAX / i; + + time1 = Plat_FloatTime(); + + for (m = 0; m < 1000; m++) + { + for (i = 0, j = 10000; i < 10000; i++, j--) + { + k = (i * a[(j % 6) + 1] ) >> PBITS; + } + } + + time2 = Plat_FloatTime(); + + DevMsg("shift & multiply = %2.5f \n", (time2-time1)); +} + +rva_t * RVA_Params ( prc_t *pprc ) +{ + rva_t *prva; + + float size_max = pprc->prm[rva_size_max]; // max delay size + float size_min = pprc->prm[rva_size_min]; // min delay size + + float numdelays = pprc->prm[rva_inumdelays]; // controls # of parallel delays + float feedback = pprc->prm[rva_ifeedback]; // 0-1.0 controls feedback parameters + float gain = pprc->prm[rva_igain]; // 0-10.0 controls output gain + + float cutoff = pprc->prm[rva_icutoff]; // filter cutoff + + float fparallel = pprc->prm[rva_ifparallel]; // if true, all filters are in delay feedback paths - otherwise single flt on output + + float fmoddly = pprc->prm[rva_imoddly]; // if > 0, milliseconds of delay mod depth + float fmodrate = pprc->prm[rva_imodrate]; // if fmoddly > 0, # of delay repetitions between modulations + + float width = fabs(pprc->prm[rva_width]); // 0-1000 controls size of 1/3 of delays - used instead of size if non-zero + float depth = fabs(pprc->prm[rva_depth]); // 0-1000 controls size of 1/3 of delays - used instead of size if non-zero + float height = fabs(pprc->prm[rva_height]); // 0-1000 controls size of 1/3 of delays - used instead of size if non-zero + + float fbwidth = pprc->prm[rva_fbwidth]; // feedback parameter for walls 0..2 + float fbdepth = pprc->prm[rva_fbdepth]; // feedback parameter for floor + float fbheight = pprc->prm[rva_fbheight]; // feedback parameter for ceiling + + float ftaps = pprc->prm[rva_iftaps]; // if > 0 increase reverb density using 3 extra taps d = (1.0 - ftaps * n) n = 0,1,2,3 + + + +// RVA_PerfTest(); + + // D array of CRVB_DLYS reverb delay sizes max sample index w[0...D] (ie: D+1 samples) + // a array of reverb feedback parms for parallel delays + // b array of CRVB_P_DLYS - mix params for parallel reverbs + // m - number of parallel delays + + int D[CRVA_DLYS]; + int a[CRVA_DLYS]; + int b[CRVA_DLYS]; + int m; + + // limit # delays 1-12 + + m = clamp (numdelays, (float)RVA_BASEM, (float)CRVA_DLYS); + + // set up D (delay) a (feedback) b (gain) arrays + + if ( int(width) || int(height) || int(depth) ) + { + // if width, height, depth given, use values as simple delays + + float rgd[3]; + float rgfb[3]; + + // force m to 3, 6, 9 or 12 + + if (m < 3) m = 3; + if (m > 3 && m < 6) m = 6; + if (m > 6 && m < 9) m = 9; + if (m > 9) m = 12; + + rgd[0] = width; rgfb[0] = fbwidth; + rgd[1] = depth; rgfb[1] = fbdepth; + rgd[2] = height; rgfb[2] = fbheight; + + RVA_ConstructDelays( rgd, rgfb, m, D, a, b, gain, feedback ); + } + else + { + // use size parameter instead of width/depth/height + + for ( int i = 0; i < m; i++ ) + { + // delays of parallel reverb. D[0] = size_min. + + D[i] = MSEC_TO_SAMPS( size_min + (int)( ((float)(size_max - size_min) / (float)m) * (float)i) ); + + // feedback and gain of parallel reverb + + if (i == 0) + { + // set feedback for smallest delay + + a[i] = (int) min (0.999 * PMAX, (double)PMAX * feedback ); + } + else + { + // adjust feedback down for larger delays so that decay time is constant + + a[i] = (int) min (0.999 * PMAX, (double)PMAX * DLY_NormalizeFeedback( D[0], feedback, D[i] ) ); + } + + b[i] = (int) ((float)(gain * PMAX) / (float)m); + } + } + + // add filter + + flt_t *pflt = NULL; + + if ( cutoff ) + { + + // set up dummy lowpass filter to convert params + + prc_t prcf; + + prcf.prm[flt_iquality] = QUA_LO; // force filter to low quality for faster execution time + prcf.prm[flt_icutoff] = cutoff; + prcf.prm[flt_iftype] = FLT_LP; + prcf.prm[flt_iqwidth] = 0; + prcf.prm[flt_igain] = 1.0; + + pflt = (flt_t *)FLT_Params ( &prcf ); + } + + prva = RVA_Alloc ( D, a, b, m, pflt, fparallel, fmoddly, fmodrate, ftaps ); + + FLT_Free( pflt ); + + return prva; +} + + +inline void * RVA_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, rva_rng ); + return (void *) RVA_Params ((prc_t *)p); +} + +inline void RVA_Mod ( void *p, float v ) { return; } + + + +//////////// +// Diffusor +/////////// + +// (N series allpass reverbs) + +#define CDFRS 64 // max number of series reverbs active + +#define CDFR_DLYS 16 // max number of delays making up diffusor + +struct dfr_t +{ + bool fused; + int n; // series allpass delays + int w[CDFR_DLYS]; // internal state array for series allpass filters + + dly_t *pdlys[CDFR_DLYS]; // array of pointers to delays +}; + +dfr_t dfrs[CDFRS]; + +void DFR_Init ( dfr_t *pdfr ) { if ( pdfr ) Q_memset (pdfr, 0, sizeof (dfr_t)); } +void DFR_InitAll( void ) { for (int i = 0; i < CDFRS; i++) DFR_Init ( &dfrs[i] ); } + +// free parallel series reverb + +void DFR_Free( dfr_t *pdfr ) +{ + if ( pdfr ) + { + // free all delays + + for (int i = 0; i < CDFR_DLYS; i++) + DLY_Free ( pdfr->pdlys[i] ); + + Q_memset( pdfr, 0, sizeof (dfr_t) ); + } +} + + +void DFR_FreeAll( void ) { for (int i = 0; i < CDFRS; i++) DFR_Free( &dfrs[i] ); } + +// create n series allpass reverbs + +// D array of CRVB_DLYS reverb delay sizes max sample index w[0...D] (ie: D+1 samples) +// a array of reverb feedback parms for series delays +// b array of gain params for parallel reverbs +// n - number of series delays + +dfr_t * DFR_Alloc ( int *D, int *a, int *b, int n ) +{ + + int i; + dfr_t *pdfr; + + // find open slot + + for (i = 0; i < CDFRS; i++) + { + if (!dfrs[i].fused) + break; + } + + // return null if no free slots + + if (i == CDFRS) + { + DevMsg ("DSP: Warning, failed to allocate diffusor.\n" ); + return NULL; + } + + pdfr = &dfrs[i]; + + DFR_Init( pdfr ); + + // alloc reverbs + + for (i = 0; i < n; i++) + pdfr->pdlys[i] = DLY_Alloc( D[i], a[i], b[i], DLY_ALLPASS ); + + // if we failed to alloc any reverb, free all, return NULL + + for (i = 0; i < n; i++) + { + if ( !pdfr->pdlys[i]) + { + DFR_Free( pdfr ); + DevMsg ("DSP: Warning, failed to allocate delay for diffusor.\n" ); + return NULL; + } + } + + pdfr->fused = true; + pdfr->n = n; + + return pdfr; +} + + +// series reverberator + +inline int DFR_GetNext( dfr_t *pdfr, int x ) +{ + int i; + int y; + dly_t *pdly; + + y = x; + + for (i = 0; i < pdfr->n; i++) + { + pdly = pdfr->pdlys[i]; + y = DelayAllpass( pdly->D, pdly->t, pdly->w, &pdly->p, pdly->a, pdly->b, y ); + } + + return y; +} + +// batch version for performance + +inline void DFR_GetNextN( dfr_t *pdfr, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = DFR_GetNext( pdfr, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = DFR_GetNext( pdfr, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = DFR_GetNext( pdfr, pb->left ); + pb++; + } + return; + } +} + +#define DFR_BASEN 1 // base number of series allpass delays + +// nominal diffusor delay and feedback values + +float dfrdlys[] = {13, 19, 26, 21, 32, 36, 38, 16, 24, 28, 41, 35, 10, 46, 50, 27}; +float dfrfbs[] = {1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0}; + + +// diffusor parameter order + +typedef enum +{ + +// parameter order + + dfr_isize, + dfr_inumdelays, + dfr_ifeedback, + dfr_igain, + + dfr_cparam // # of params + +} dfr_e; + +// diffusor parameter ranges + +prm_rng_t dfr_rng[] = { + + {dfr_cparam, 0, 0}, // first entry is # of parameters + + {dfr_isize, 0.0, 1.0}, // 0-1.0 scales all delays + {dfr_inumdelays,0.0, 4.0}, // 0-4.0 controls # of series delays + {dfr_ifeedback, 0.0, 1.0}, // 0-1.0 scales all feedback parameters + {dfr_igain, 0.0, 10.0}, // 0-1.0 scales all feedback parameters +}; + + +dfr_t * DFR_Params ( prc_t *pprc ) +{ + dfr_t *pdfr; + int i; + int s; + float size = pprc->prm[dfr_isize]; // 0-1.0 scales all delays + float numdelays = pprc->prm[dfr_inumdelays]; // 0-4.0 controls # of series delays + float feedback = pprc->prm[dfr_ifeedback]; // 0-1.0 scales all feedback parameters + float gain = pprc->prm[dfr_igain]; // 0-10.0 controls output gain + + // D array of CRVB_DLYS reverb delay sizes max sample index w[0...D] (ie: D+1 samples) + // a array of reverb feedback parms for series delays (CRVB_S_DLYS) + // b gain of each reverb section + // n - number of series delays + + int D[CDFR_DLYS]; + int a[CDFR_DLYS]; + int b[CDFR_DLYS]; + int n; + + if (gain == 0.0) + gain = 1.0; + + // get # series diffusors + + // limit m, n to half max number of delays + + n = clamp (Float2Int(numdelays), DFR_BASEN, CDFR_DLYS/2); + + // compute delays for diffusors + + for (i = 0; i < n; i++) + { + s = (int)( dfrdlys[i] * size ); + + // delay of diffusor + + D[i] = MSEC_TO_SAMPS(s); + + // feedback and gain of diffusor + + a[i] = min (0.999 * PMAX, (double)(dfrfbs[i] * PMAX * feedback)); + b[i] = (int) ( (float)(gain * (float)PMAX) ); + } + + + pdfr = DFR_Alloc ( D, a, b, n ); + + return pdfr; +} + +inline void * DFR_VParams ( void *p ) +{ + PRC_CheckParams ((prc_t *)p, dfr_rng); + return (void *) DFR_Params ((prc_t *)p); +} + +inline void DFR_Mod ( void *p, float v ) { return; } + + +////////////////////// +// LFO wav definitions +////////////////////// + +#define CLFOSAMPS 512 // samples per wav table - single cycle only +#define LFOBITS 14 // bits of peak amplitude of lfo wav +#define LFOAMP ((1<<LFOBITS)-1) // peak amplitude of lfo wav + +//types of lfo wavs + +#define LFO_SIN 0 // sine wav +#define LFO_TRI 1 // triangle wav +#define LFO_SQR 2 // square wave, 50% duty cycle +#define LFO_SAW 3 // forward saw wav +#define LFO_RND 4 // random wav +#define LFO_LOG_IN 5 // logarithmic fade in +#define LFO_LOG_OUT 6 // logarithmic fade out +#define LFO_LIN_IN 7 // linear fade in +#define LFO_LIN_OUT 8 // linear fade out +#define LFO_MAX LFO_LIN_OUT + +#define CLFOWAV 9 // number of LFO wav tables + +struct lfowav_t // lfo or envelope wave table +{ + int type; // lfo type + dly_t *pdly; // delay holds wav values and step pointers +}; + +lfowav_t lfowavs[CLFOWAV]; + +// deallocate lfo wave table. Called only when sound engine exits. + +void LFOWAV_Free( lfowav_t *plw ) +{ + // free delay + + if ( plw ) + DLY_Free( plw->pdly ); + + Q_memset( plw, 0, sizeof (lfowav_t) ); +} + +// deallocate all lfo wave tables. Called only when sound engine exits. + +void LFOWAV_FreeAll( void ) +{ + for ( int i = 0; i < CLFOWAV; i++ ) + LFOWAV_Free( &lfowavs[i] ); +} + +// fill lfo array w with count samples of lfo type 'type' +// all lfo wavs except fade out, rnd, and log_out should start with 0 output + +void LFOWAV_Fill( int *w, int count, int type ) +{ + int i,x; + switch (type) + { + default: + case LFO_SIN: // sine wav, all values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++ ) + { + x = ( int )( (float)(LFOAMP) * sinf( (2.0 * M_PI_F * (float)i / (float)count ) + (M_PI_F * 1.5) ) ); + w[i] = (x + LFOAMP)/2; + } + break; + case LFO_TRI: // triangle wav, all values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++) + { + w[i] = ( int ) ( (float)(2 * LFOAMP * i ) / (float)(count) ); + + if ( i > count / 2 ) + w[i] = ( int ) ( (float) (2 * LFOAMP) - (float)( 2 * LFOAMP * i ) / (float)(count) ); + } + break; + case LFO_SQR: // square wave, 50% duty cycle, all values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++) + w[i] = i > count / 2 ? 0 : LFOAMP; + break; + case LFO_SAW: // forward saw wav, aall values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++) + w[i] = ( int ) ( (float)(LFOAMP) * (float)i / (float)(count) ); + break; + case LFO_RND: // random wav, all values 0 <= x <= LFOAMP + for (i = 0; i < count; i++) + w[i] = ( int ) ( RandomInt(0, LFOAMP) ); + break; + case LFO_LOG_IN: // logarithmic fade in, all values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++) + w[i] = ( int ) ( (float)(LFOAMP) * powf( (float)i / (float)count, 2)); + break; + case LFO_LOG_OUT: // logarithmic fade out, all values 0 <= x <= LFOAMP, initial value = LFOAMP + for (i = 0; i < count; i++) + w[i] = ( int ) ( (float)(LFOAMP) * powf( 1.0 - ((float)i / (float)count), 2 )); + break; + case LFO_LIN_IN: // linear fade in, all values 0 <= x <= LFOAMP, initial value = 0 + for (i = 0; i < count; i++) + w[i] = ( int ) ( (float)(LFOAMP) * (float)i / (float)(count) ); + break; + case LFO_LIN_OUT: // linear fade out, all values 0 <= x <= LFOAMP, initial value = LFOAMP + for (i = 0; i < count; i++) + w[i] = LFOAMP - ( int ) ( (float)(LFOAMP) * (float)i / (float)(count) ); + break; + } +} + +// allocate all lfo wave tables. Called only when sound engine loads. + +void LFOWAV_InitAll() +{ + int i; + dly_t *pdly; + + Q_memset( lfowavs, 0, sizeof( lfowavs ) ); + + // alloc space for each lfo wav type + + for (i = 0; i < CLFOWAV; i++) + { + pdly = DLY_Alloc( CLFOSAMPS, 0, 0 , DLY_PLAIN); + + lfowavs[i].pdly = pdly; + lfowavs[i].type = i; + + LFOWAV_Fill( pdly->w, CLFOSAMPS, i ); + } + + // if any dlys fail to alloc, free all + + for (i = 0; i < CLFOWAV; i++) + { + if ( !lfowavs[i].pdly ) + LFOWAV_FreeAll(); + } +} + + +//////////////////////////////////////// +// LFO iterators - one shot and looping +//////////////////////////////////////// + +#define CLFO 16 // max active lfos (this steals from active delays) + +struct lfo_t +{ + bool fused; // true if slot take + + dly_t *pdly; // delay points to lfo wav within lfowav_t (don't free this) + + int gain; + + float f; // playback frequency in hz + + pos_t pos; // current position within wav table, looping + pos_one_t pos1; // current position within wav table, one shot + + int foneshot; // true - one shot only, don't repeat +}; + +lfo_t lfos[CLFO]; + +void LFO_Init( lfo_t *plfo ) { if ( plfo ) Q_memset( plfo, 0, sizeof (lfo_t) ); } +void LFO_InitAll( void ) { for (int i = 0; i < CLFO; i++) LFO_Init(&lfos[i]); } +void LFO_Free( lfo_t *plfo ) { if ( plfo ) Q_memset( plfo, 0, sizeof (lfo_t) ); } +void LFO_FreeAll( void ) { for (int i = 0; i < CLFO; i++) LFO_Free(&lfos[i]); } + + +// get step value given desired playback frequency + +inline float LFO_HzToStep ( float freqHz ) +{ + float lfoHz; + + // calculate integer and fractional step values, + // assume an update rate of SOUND_DMA_SPEED samples/sec + + // 1 cycle/CLFOSAMPS * SOUND_DMA_SPEED samps/sec = cycles/sec = current lfo rate + // + // lforate * X = freqHz so X = freqHz/lforate = update rate + + lfoHz = (float)(SOUND_DMA_SPEED) / (float)(CLFOSAMPS); + + return freqHz / lfoHz; +} + +// return pointer to new lfo + +lfo_t * LFO_Alloc( int wtype, float freqHz, bool foneshot, float gain ) +{ + int i; + int type = min ( CLFOWAV - 1, wtype ); + float lfostep; + + for (i = 0; i < CLFO; i++) + if (!lfos[i].fused) + { + lfo_t *plfo = &lfos[i]; + + LFO_Init( plfo ); + + plfo->fused = true; + plfo->pdly = lfowavs[type].pdly; // pdly in lfo points to wav table data in lfowavs + plfo->f = freqHz; + plfo->foneshot = foneshot; + plfo->gain = gain * PMAX; + + lfostep = LFO_HzToStep( freqHz ); + + // init positional pointer (ie: fixed point updater for controlling pitch of lfo) + + if ( !foneshot ) + POS_Init(&(plfo->pos), plfo->pdly->D, lfostep ); + else + POS_ONE_Init(&(plfo->pos1), plfo->pdly->D,lfostep ); + + return plfo; + } + DevMsg ("DSP: Warning, failed to allocate LFO.\n" ); + return NULL; +} + +// get next lfo value +// Value returned is 0..LFOAMP. can be normalized by shifting right by LFOBITS +// To play back at correct passed in frequency, routien should be +// called once for every output sample (ie: at SOUND_DMA_SPEED) +// x is dummy param + +inline int LFO_GetNext( lfo_t *plfo, int x ) +{ + int i; + + // get current position + + if ( !plfo->foneshot ) + i = POS_GetNext( &plfo->pos ); + else + i = POS_ONE_GetNext( &plfo->pos1 ); + + // return current sample + + if (plfo->gain == PMAX) + return plfo->pdly->w[i]; + else + return (plfo->pdly->w[i] * plfo->gain ) >> PBITS; +} + +// batch version for performance + +inline void LFO_GetNextN( lfo_t *plfo, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = LFO_GetNext( plfo, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = LFO_GetNext( plfo, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = LFO_GetNext( plfo, pb->left ); + pb++; + } + return; + } +} + +// uses lfowav, rate, foneshot + +typedef enum +{ + +// parameter order + + lfo_iwav, + lfo_irate, + lfo_ifoneshot, + lfo_igain, + + lfo_cparam // # of params + +} lfo_e; + +// parameter ranges + +prm_rng_t lfo_rng[] = { + + {lfo_cparam, 0, 0}, // first entry is # of parameters + + {lfo_iwav, 0.0, LFO_MAX}, // lfo type to use (LFO_SIN, LFO_RND...) + {lfo_irate, 0.0, 16000.0}, // modulation rate in hz. for MDY, 1/rate = 'glide' time in seconds + {lfo_ifoneshot, 0.0, 1.0}, // 1.0 if lfo is oneshot + {lfo_igain, 0.0, 10.0}, // output gain +}; + + +lfo_t * LFO_Params ( prc_t *pprc ) +{ + lfo_t *plfo; + bool foneshot = pprc->prm[lfo_ifoneshot] > 0 ? true : false; + float gain = pprc->prm[lfo_igain]; + + plfo = LFO_Alloc ( pprc->prm[lfo_iwav], pprc->prm[lfo_irate], foneshot, gain ); + + return plfo; +} + +void LFO_ChangeVal ( lfo_t *plfo, float fhz ) +{ + float fstep = LFO_HzToStep( fhz ); + + // change lfo playback rate to new frequency fhz + + if ( plfo->foneshot ) + POS_ChangeVal( &plfo->pos, fstep ); + else + POS_ChangeVal( &plfo->pos1.p, fstep ); +} + +inline void * LFO_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, lfo_rng ); + return (void *) LFO_Params ((prc_t *)p); +} + +// v is +/- 0-1.0 +// v changes current lfo frequency up/down by +/- v% + +inline void LFO_Mod ( lfo_t *plfo, float v ) +{ + float fhz; + float fhznew; + + fhz = plfo->f; + fhznew = fhz * (1.0 + v); + + LFO_ChangeVal ( plfo, fhznew ); + + return; +} + + +//////////////////////////////////////// +// Time Compress/expand with pitch shift +//////////////////////////////////////// + +// realtime pitch shift - ie: pitch shift without change to playback rate + +#define CPTCS 64 + +struct ptc_t +{ + bool fused; + + dly_t *pdly_in; // input buffer space + dly_t *pdly_out; // output buffer space + + int *pin; // input buffer (pdly_in->w) + int *pout; // output buffer (pdly_out->w) + + int cin; // # samples in input buffer + int cout; // # samples in output buffer + + int cxfade; // # samples in crossfade segment + int ccut; // # samples to cut + int cduplicate; // # samples to duplicate (redundant - same as ccut) + + int iin; // current index into input buffer (reading) + + pos_one_t psn; // stepping index through output buffer + + bool fdup; // true if duplicating, false if cutting + + float fstep; // pitch shift & time compress/expand +}; + +ptc_t ptcs[CPTCS]; + +void PTC_Init( ptc_t *pptc ) { if (pptc) Q_memset( pptc, 0, sizeof (ptc_t) ); }; +void PTC_Free( ptc_t *pptc ) +{ + if (pptc) + { + DLY_Free (pptc->pdly_in); + DLY_Free (pptc->pdly_out); + + Q_memset( pptc, 0, sizeof (ptc_t) ); + } +}; +void PTC_InitAll() { for (int i = 0; i < CPTCS; i++) PTC_Init( &ptcs[i] ); }; +void PTC_FreeAll() { for (int i = 0; i < CPTCS; i++) PTC_Free( &ptcs[i] ); }; + + + +// Time compressor/expander with pitch shift (ie: pitch changes, playback rate does not) +// +// Algorithm: + +// 1) Duplicate or discard chunks of sound to provide tslice * fstep seconds of sound. +// (The user-selectable size of the buffer to process is tslice milliseconds in length) +// 2) Resample this compressed/expanded buffer at fstep to produce a pitch shifted +// output with the same duration as the input (ie: #samples out = # samples in, an +// obvious requirement for realtime inline processing). + +// timeslice is size in milliseconds of full buffer to process. +// timeslice * fstep is the size of the expanded/compressed buffer +// timexfade is length in milliseconds of crossfade region between duplicated or cut sections +// fstep is % expanded/compressed sound normalized to 0.01-2.0 (1% - 200%) + +// input buffer: + +// iin--> + +// [0... tslice ...D] input samples 0...D (D is NEWEST sample) +// [0... ...n][m... tseg ...D] region to be cut or duplicated m...D + +// [0... [p..txf1..n][m... tseg ...D] fade in region 1 txf1 p...n +// [0... ...n][m..[q..txf2..D] fade out region 2 txf2 q...D + + +// pitch up: duplicate into output buffer: tdup = tseg + +// [0... ...n][m... tdup ...D][m... tdup ...D] output buffer size with duplicate region +// [0... ...n][m..[p...xf1..n][m... tdup ...D] fade in p...n while fading out q...D +// [0... ...n][m..[q...xf2..D][m... tdup ...D] +// [0... ...n][m..[.XFADE...n][m... tdup ...D] final duplicated output buffer - resample at fstep + +// pitch down: cut into output buffer: tcut = tseg + +// [0... ...n][m... tcut ...D] input samples with cut region delineated m...D +// [0... ...n] output buffer size after cut +// [0... [q..txf2...D] fade in txf1 q...D while fade out txf2 p...n +// [0... [.XFADE ...D] final cut output buffer - resample at fstep + + +ptc_t * PTC_Alloc( float timeslice, float timexfade, float fstep ) +{ + + int i; + ptc_t *pptc; + float tout; + int cin, cout; + float tslice = timeslice; + float txfade = timexfade; + float tcutdup; + + // find time compressor slot + + for ( i = 0; i < CPTCS; i++ ) + { + if ( !ptcs[i].fused ) + break; + } + + if ( i == CPTCS ) + { + DevMsg ("DSP: Warning, failed to allocate pitch shifter.\n" ); + return NULL; + } + + pptc = &ptcs[i]; + + PTC_Init ( pptc ); + + // get size of region to cut or duplicate + + tcutdup = abs((fstep - 1.0) * timeslice); + + // to prevent buffer overruns: + + // make sure timeslice is greater than cut/dup time + + tslice = max ( (double)tslice, 1.1 * tcutdup); + + // make sure xfade time smaller than cut/dup time, and smaller than (timeslice-cutdup) time + + txfade = min ( (double)txfade, 0.9 * tcutdup ); + txfade = min ( (double)txfade, 0.9 * (tslice - tcutdup)); + + pptc->cxfade = MSEC_TO_SAMPS( txfade ); + pptc->ccut = MSEC_TO_SAMPS( tcutdup ); + pptc->cduplicate = MSEC_TO_SAMPS( tcutdup ); + + // alloc delay lines (buffers) + + tout = tslice * fstep; + + cin = MSEC_TO_SAMPS( tslice ); + cout = MSEC_TO_SAMPS( tout ); + + pptc->pdly_in = DLY_Alloc( cin, 0, 1, DLY_LINEAR ); // alloc input buffer + pptc->pdly_out = DLY_Alloc( cout, 0, 1, DLY_LINEAR); // alloc output buffer + + if ( !pptc->pdly_in || !pptc->pdly_out ) + { + PTC_Free( pptc ); + DevMsg ("DSP: Warning, failed to allocate delay for pitch shifter.\n" ); + return NULL; + } + + // buffer pointers + + pptc->pin = pptc->pdly_in->w; + pptc->pout = pptc->pdly_out->w; + + // input buffer index + + pptc->iin = 0; + + // output buffer index + + POS_ONE_Init ( &pptc->psn, cout, fstep ); + + // if fstep > 1.0 we're pitching shifting up, so fdup = true + + pptc->fdup = fstep > 1.0 ? true : false; + + pptc->cin = cin; + pptc->cout = cout; + + pptc->fstep = fstep; + pptc->fused = true; + + return pptc; +} + +// linear crossfader +// yfadein - instantaneous value fading in +// ydafeout -instantaneous value fading out +// nsamples - duration in #samples of fade +// isample - index in to fade 0...nsamples-1 + +inline int xfade ( int yfadein, int yfadeout, int nsamples, int isample ) +{ + int yout; + int m = (isample << PBITS ) / nsamples; + +// yout = ((yfadein * m) >> PBITS) + ((yfadeout * (PMAX - m)) >> PBITS); + yout = (yfadeout + (yfadein - yfadeout) * m ) >> PBITS; + + return yout; +} + +// w - pointer to start of input buffer samples +// v - pointer to start of output buffer samples +// cin - # of input buffer samples +// cout = # of output buffer samples +// cxfade = # of crossfade samples +// cduplicate = # of samples in duplicate/cut segment + +void TimeExpand( int *w, int *v, int cin, int cout, int cxfade, int cduplicate ) +{ + int i,j; + int m; + int p; + int q; + int D; + + // input buffer + // xfade source duplicate + // [0...........][p.......n][m...........D] + + // output buffer + // xfade region duplicate + // [0.....................n][m..[q.......D][m...........D] + + // D - index of last sample in input buffer + // m - index of 1st sample in duplication region + // p - index of 1st sample of crossfade source + // q - index of 1st sample in crossfade region + + D = cin - 1; + m = cin - cduplicate; + p = m - cxfade; + q = cin - cxfade; + + // copy up to crossfade region + + for (i = 0; i < q; i++) + v[i] = w[i]; + + // crossfade region + + j = p; + + for (i = q; i <= D; i++) + v[i] = xfade (w[j++], w[i], cxfade, i-q); // fade out p..n, fade in q..D + + // duplicate region + + j = D+1; + + for (i = m; i <= D; i++) + v[j++] = w[i]; + +} + +// cut ccut samples from end of input buffer, crossfade end of cut section +// with end of remaining section + +// w - pointer to start of input buffer samples +// v - pointer to start of output buffer samples +// cin - # of input buffer samples +// cout = # of output buffer samples +// cxfade = # of crossfade samples +// ccut = # of samples in cut segment + +void TimeCompress( int *w, int *v, int cin, int cout, int cxfade, int ccut ) +{ + int i,j; + int m; + int p; + int q; + int D; + + // input buffer + // xfade source + // [0.....................n][m..[p.......D] + + // xfade region cut + // [0...........][q.......n][m...........D] + + // output buffer + // xfade to source + // [0...........][p.......D] + + // D - index of last sample in input buffer + // m - index of 1st sample in cut region + // p - index of 1st sample of crossfade source + // q - index of 1st sample in crossfade region + + D = cin - 1; + m = cin - ccut; + p = cin - cxfade; + q = m - cxfade; + + // copy up to crossfade region + + for (i = 0; i < q; i++) + v[i] = w[i]; + + // crossfade region + + j = p; + + for (i = q; i < m; i++) + v[i] = xfade (w[j++], w[i], cxfade, i-q); // fade out p..n, fade in q..D + + // skip rest of input buffer +} + +// get next sample + +// put input sample into input (delay) buffer +// get output sample from output buffer, step by fstep % +// output buffer is time expanded or compressed version of previous input buffer + +inline int PTC_GetNext( ptc_t *pptc, int x ) +{ + int iout, xout; + bool fhitend = false; + + // write x into input buffer + Assert (pptc->iin < pptc->cin); + + pptc->pin[pptc->iin] = x; + + pptc->iin++; + + // check for end of input buffer + + if ( pptc->iin >= pptc->cin ) + fhitend = true; + + // read sample from output buffer, resampling at fstep + + iout = POS_ONE_GetNext( &pptc->psn ); + Assert (iout < pptc->cout); + xout = pptc->pout[iout]; + + if ( fhitend ) + { + // if hit end of input buffer (ie: input buffer is full) + // reset input buffer pointer + // reset output buffer pointer + // rebuild entire output buffer (TimeCompress/TimeExpand) + + pptc->iin = 0; + + POS_ONE_Init( &pptc->psn, pptc->cout, pptc->fstep ); + + if ( pptc->fdup ) + TimeExpand ( pptc->pin, pptc->pout, pptc->cin, pptc->cout, pptc->cxfade, pptc->cduplicate ); + else + TimeCompress ( pptc->pin, pptc->pout, pptc->cin, pptc->cout, pptc->cxfade, pptc->ccut ); + } + + return xout; +} + +// batch version for performance + +inline void PTC_GetNextN( ptc_t *pptc, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = PTC_GetNext( pptc, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = PTC_GetNext( pptc, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = PTC_GetNext( pptc, pb->left ); + pb++; + } + return; + } +} + +// change time compression to new value +// fstep is new value +// ramptime is how long change takes in seconds (ramps smoothly), 0 for no ramp + +void PTC_ChangeVal( ptc_t *pptc, float fstep, float ramptime ) +{ +// UNDONE: ignored +// UNDONE: just realloc time compressor with new fstep +} + +// uses pitch: +// 1.0 = playback normal rate +// 0.5 = cut 50% of sound (2x playback) +// 1.5 = add 50% sound (0.5x playback) + +typedef enum +{ + +// parameter order + + ptc_ipitch, + ptc_itimeslice, + ptc_ixfade, + + ptc_cparam // # of params + +} ptc_e; + +// diffusor parameter ranges + +prm_rng_t ptc_rng[] = { + + {ptc_cparam, 0, 0}, // first entry is # of parameters + + {ptc_ipitch, 0.1, 4.0}, // 0-n.0 where 1.0 = 1 octave up and 0.5 is one octave down + {ptc_itimeslice, 20.0, 300.0}, // in milliseconds - size of sound chunk to analyze and cut/duplicate - 100ms nominal + {ptc_ixfade, 1.0, 200.0}, // in milliseconds - size of crossfade region between spliced chunks - 20ms nominal +}; + + +ptc_t * PTC_Params ( prc_t *pprc ) +{ + ptc_t *pptc; + + float pitch = pprc->prm[ptc_ipitch]; + float timeslice = pprc->prm[ptc_itimeslice]; + float txfade = pprc->prm[ptc_ixfade]; + + pptc = PTC_Alloc( timeslice, txfade, pitch ); + + return pptc; +} + +inline void * PTC_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, ptc_rng ); + return (void *) PTC_Params ((prc_t *)p); +} + +// change to new pitch value +// v is +/- 0-1.0 +// v changes current pitch up/down by +/- v% + +void PTC_Mod ( ptc_t *pptc, float v ) +{ + float fstep; + float fstepnew; + + fstep = pptc->fstep; + fstepnew = fstep * (1.0 + v); + + PTC_ChangeVal( pptc, fstepnew, 0.01 ); +} + + +//////////////////// +// ADSR envelope +//////////////////// + +#define CENVS 64 // max # of envelopes active +#define CENVRMPS 4 // A, D, S, R + +#define ENV_LIN 0 // linear a,d,s,r +#define ENV_EXP 1 // exponential a,d,s,r +#define ENV_MAX ENV_EXP + +#define ENV_BITS 14 // bits of resolution of ramp + +struct env_t +{ + bool fused; + + bool fhitend; // true if done + bool fexp; // true if exponential ramps + + int ienv; // current ramp + rmp_t rmps[CENVRMPS]; // ramps +}; + +env_t envs[CENVS]; + +void ENV_Init( env_t *penv ) { if (penv) Q_memset( penv, 0, sizeof (env_t) ); }; +void ENV_Free( env_t *penv ) { if (penv) Q_memset( penv, 0, sizeof (env_t) ); }; +void ENV_InitAll() { for (int i = 0; i < CENVS; i++) ENV_Init( &envs[i] ); }; +void ENV_FreeAll() { for (int i = 0; i < CENVS; i++) ENV_Free( &envs[i] ); }; + + +// allocate ADSR envelope +// all times are in seconds +// amp1 - attack amplitude multiplier 0-1.0 +// amp2 - sustain amplitude multiplier 0-1.0 +// amp3 - end of sustain amplitude multiplier 0-1.0 + +env_t *ENV_Alloc ( int type, float famp1, float famp2, float famp3, float attack, float decay, float sustain, float release, bool fexp) +{ + int i; + env_t *penv; + + for (i = 0; i < CENVS; i++) + { + if ( !envs[i].fused ) + { + + int amp1 = famp1 * (1 << ENV_BITS); // ramp resolution + int amp2 = famp2 * (1 << ENV_BITS); + int amp3 = famp3 * (1 << ENV_BITS); + + penv = &envs[i]; + + ENV_Init (penv); + + // UNDONE: ignoring type = ENV_EXP - use oneshot LFOS instead with sawtooth/exponential + + // set up ramps + + RMP_Init( &penv->rmps[0], attack, 0, amp1, true ); + RMP_Init( &penv->rmps[1], decay, amp1, amp2, true ); + RMP_Init( &penv->rmps[2], sustain, amp2, amp3, true ); + RMP_Init( &penv->rmps[3], release, amp3, 0, true ); + + penv->ienv = 0; + penv->fused = true; + penv->fhitend = false; + penv->fexp = fexp; + return penv; + } + } + DevMsg ("DSP: Warning, failed to allocate envelope.\n" ); + return NULL; +} + + +inline int ENV_GetNext( env_t *penv, int x ) +{ + if ( !penv->fhitend ) + { + int i; + int y; + + i = penv->ienv; + y = RMP_GetNext ( &penv->rmps[i] ); + + // check for next ramp + + if ( penv->rmps[i].fhitend ) + i++; + + penv->ienv = i; + + // check for end of all ramps + + if ( i > 3) + penv->fhitend = true; + + // multiply input signal by ramp + + if (penv->fexp) + return (((x * y) >> ENV_BITS) * y) >> ENV_BITS; + else + return (x * y) >> ENV_BITS; + } + + return 0; +} + +// batch version for performance + +inline void ENV_GetNextN( env_t *penv, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = ENV_GetNext( penv, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = ENV_GetNext( penv, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = ENV_GetNext( penv, pb->left ); + pb++; + } + return; + } +} + +// uses lfowav, amp1, amp2, amp3, attack, decay, sustain, release +// lfowav is type, currently ignored - ie: LFO_LIN_IN, LFO_LOG_IN + +// parameter order + +typedef enum +{ + env_itype, + env_iamp1, + env_iamp2, + env_iamp3, + env_iattack, + env_idecay, + env_isustain, + env_irelease, + env_ifexp, + env_cparam // # of params + +} env_e; + +// parameter ranges + +prm_rng_t env_rng[] = { + + {env_cparam, 0, 0}, // first entry is # of parameters + + {env_itype, 0.0,ENV_MAX}, // ENV_LINEAR, ENV_LOG - currently ignored + {env_iamp1, 0.0, 1.0}, // attack peak amplitude 0-1.0 + {env_iamp2, 0.0, 1.0}, // decay target amplitued 0-1.0 + {env_iamp3, 0.0, 1.0}, // sustain target amplitude 0-1.0 + {env_iattack, 0.0, 20000.0}, // attack time in milliseconds + {env_idecay, 0.0, 20000.0}, // envelope decay time in milliseconds + {env_isustain, 0.0, 20000.0}, // sustain time in milliseconds + {env_irelease, 0.0, 20000.0}, // release time in milliseconds + {env_ifexp, 0.0, 1.0}, // 1.0 if exponential ramps +}; + +env_t * ENV_Params ( prc_t *pprc ) +{ + env_t *penv; + + float type = pprc->prm[env_itype]; + float amp1 = pprc->prm[env_iamp1]; + float amp2 = pprc->prm[env_iamp2]; + float amp3 = pprc->prm[env_iamp3]; + float attack = pprc->prm[env_iattack]/1000.0; + float decay = pprc->prm[env_idecay]/1000.0; + float sustain = pprc->prm[env_isustain]/1000.0; + float release = pprc->prm[env_irelease]/1000.0; + float fexp = pprc->prm[env_ifexp]; + bool bexp; + + bexp = fexp > 0.0 ? 1 : 0; + penv = ENV_Alloc ( type, amp1, amp2, amp3, attack, decay, sustain, release, bexp ); + return penv; +} + +inline void * ENV_VParams ( void *p ) +{ + PRC_CheckParams( (prc_t *)p, env_rng ); + return (void *) ENV_Params ((prc_t *)p); +} + +inline void ENV_Mod ( void *p, float v ) { return; } + +////////////////////////// +// Gate & envelope follower +////////////////////////// + +#define CEFOS 64 // max # of envelope followers active + +struct efo_t +{ + bool fused; + + int xout; // current output value + + // gate params + + bool bgate; // if true, gate function is on + + bool bgateon; // if true, gate is on + bool bexp; // if true, use exponential fade out + + int thresh; // amplitude threshold for gate on + int thresh_off; // amplitidue threshold for gate off + + float attack_time; // gate attack time in seconds + float decay_time; // gate decay time in seconds + + rmp_t rmp_attack; // gate on ramp - attack + rmp_t rmp_decay; // gate off ramp - decay +}; + +efo_t efos[CEFOS]; + +void EFO_Init( efo_t *pefo ) { if (pefo) Q_memset( pefo, 0, sizeof (efo_t) ); }; +void EFO_Free( efo_t *pefo ) { if (pefo) Q_memset( pefo, 0, sizeof (efo_t) ); }; +void EFO_InitAll() { for (int i = 0; i < CEFOS; i++) EFO_Init( &efos[i] ); }; +void EFO_FreeAll() { for (int i = 0; i < CEFOS; i++) EFO_Free( &efos[i] ); }; + +// return true when gate is off AND decay ramp has hit end + +inline bool EFO_GateOff( efo_t *pefo ) +{ + return ( !pefo->bgateon && RMP_HitEnd( &pefo->rmp_decay ) ); +} + + +// allocate enveloper follower + +#define EFO_HYST_AMP 1000 // hysteresis amplitude + +efo_t *EFO_Alloc ( float threshold, float attack_sec, float decay_sec, bool bexp ) +{ + int i; + efo_t *pefo; + + for (i = 0; i < CEFOS; i++) + { + if ( !efos[i].fused ) + { + pefo = &efos[i]; + + EFO_Init ( pefo ); + + pefo->xout = 0; + pefo->fused = true; + + // init gate params + + pefo->bgate = threshold > 0.0; + + if (pefo->bgate) + { + pefo->attack_time = attack_sec; + pefo->decay_time = decay_sec; + + RMP_Init( &pefo->rmp_attack, attack_sec, 0, PMAX, false); + RMP_Init( &pefo->rmp_decay, decay_sec, PMAX, 0, false); + RMP_SetEnd( &pefo->rmp_attack ); + RMP_SetEnd( &pefo->rmp_decay ); + + pefo->thresh = threshold; + pefo->thresh_off = max(1.f, threshold - EFO_HYST_AMP); + pefo->bgateon = false; + pefo->bexp = bexp; + } + + return pefo; + } + } + + DevMsg ("DSP: Warning, failed to allocate envelope follower.\n" ); + return NULL; +} + +// values of L for CEFO_BITS_DIVIDE: L = (1 - 1/(1 << CEFO_BITS_DIVIDE)) +// 1 L = 0.5 +// 2 L = 0.75 +// 3 L = 0.875 +// 4 L = 0.9375 +// 5 L = 0.96875 +// 6 L = 0.984375 +// 7 L = 0.9921875 +// 8 L = 0.99609375 +// 9 L = 0.998046875 +// 10 L = 0.9990234375 +// 11 L = 0.99951171875 +// 12 L = 0.999755859375 + + +// decay time constant for values of L, for E = 10^-3 = 60dB of attenuation +// +// Neff = Ln E / Ln L = -6.9077552 / Ln L +// +// 1 L = 0.5 Neff = 10 samples +// 2 L = 0.75 Neff = 24 +// 3 L = 0.875 Neff = 51 +// 4 L = 0.9375 Neff = 107 +// 5 L = 0.96875 Neff = 217 +// 6 L = 0.984375 Neff = 438 +// 7 L = 0.9921875 Neff = 880 +// 8 L = 0.99609375 Neff = 1764 +// 9 L = 0.998046875 Neff = 3533 +// 10 L = 0.9990234375 Neff = 7070 +// 11 L = 0.99951171875 Neff = 14143 +// 12 L = 0.999755859375 Neff = 28290 + +#define CEFO_BITS 11 // 14143 samples in gate window (3hz) + +inline int EFO_GetNext( efo_t *pefo, int x ) +{ + int r; + int xa = abs(x); + int xdif; + + + // get envelope: + // Cn = L * Cn-1 + ( 1 - L ) * |x| + + // which simplifies to: + // Cn = |x| + (Cn-1 - |x|) * L + + // for 0 < L < 1 + + // increasing L increases time to rise or fall to a new input level + + // so: increasing CEFO_BITS_DIVIDE increases rise/fall time + + // where: L = (1 - 1/(1 << CEFO_BITS)) + // xdif = Cn-1 - |x| + // so: xdif * L = xdif - xdif / (1 << CEFO_BITS) = ((xdif << CEFO_BITS) - xdif ) >> CEFO_BITS + + xdif = pefo->xout - xa; + + pefo->xout = xa + (((xdif << CEFO_BITS) - xdif) >> CEFO_BITS); + + if ( pefo->bgate ) + { + // gate + + bool bgateon_prev = pefo->bgateon; + + // gate hysteresis + + if (bgateon_prev) + // gate was on - it's off only if amp drops below thresh_off + pefo->bgateon = ( pefo->xout >= pefo->thresh_off ); + else + // gate was off - it's on only if amp > thresh + pefo->bgateon = ( pefo->xout >= pefo->thresh ); + + if ( pefo->bgateon ) + { + // gate is on + + if ( bgateon_prev && RMP_HitEnd( &pefo->rmp_attack )) + return x; // gate is fully on + + if ( !bgateon_prev ) + { + // gate just turned on, start ramp attack + + // start attack from previous decay ramp if active + + r = RMP_HitEnd( &pefo->rmp_decay ) ? 0 : RMP_GetNext( &pefo->rmp_decay ); + RMP_SetEnd( &pefo->rmp_decay); + + // DevMsg ("GATE ON \n"); + + RMP_Init( &pefo->rmp_attack, pefo->attack_time, r, PMAX, false); + + return (x * r) >> PBITS; + } + + if ( !RMP_HitEnd( &pefo->rmp_attack ) ) + { + r = RMP_GetNext( &pefo->rmp_attack ); + + // gate is on and ramping up + + return (x * r) >> PBITS; + } + + } + else + { + // gate is fully off + + if ( !bgateon_prev && RMP_HitEnd( &pefo->rmp_decay)) + return 0; + + if ( bgateon_prev ) + { + // gate just turned off, start ramp decay + + // start decay from previous attack ramp if active + + r = RMP_HitEnd( &pefo->rmp_attack ) ? PMAX : RMP_GetNext( &pefo->rmp_attack ); + RMP_SetEnd( &pefo->rmp_attack); + + RMP_Init( &pefo->rmp_decay, pefo->decay_time, r, 0, false); + + // DevMsg ("GATE OFF \n"); + + // if exponential set, gate has exponential ramp down. Otherwise linear ramp down. + + if ( pefo->bexp ) + return ( (((x * r) >> PBITS) * r ) >> PBITS ); + else + return (x * r) >> PBITS; + + } + else if ( !RMP_HitEnd( &pefo->rmp_decay ) ) + { + // gate is off and ramping down + + r = RMP_GetNext( &pefo->rmp_decay ); + + + // if exponential set, gate has exponential ramp down. Otherwise linear ramp down. + + if ( pefo->bexp ) + return ( (((x * r) >> PBITS) * r ) >> PBITS ); + else + return (x * r) >> PBITS; + } + } + + return x; + } + + return pefo->xout; +} + +// batch version for performance + +inline void EFO_GetNextN( efo_t *pefo, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = EFO_GetNext( pefo, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = EFO_GetNext( pefo, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = EFO_GetNext( pefo, pb->left ); + pb++; + } + return; + } +} +// parameter order + +typedef enum +{ + efo_ithreshold, + efo_iattack, + efo_idecay, + efo_iexp, + + efo_cparam // # of params + +} efo_e; + +// parameter ranges + +prm_rng_t efo_rng[] = { + + {efo_cparam, 0, 0}, // first entry is # of parameters + + {efo_ithreshold, -140.0, 0.0}, // gate threshold in db. if 0.0 then no gate. + {efo_iattack, 0.0, 20000.0}, // attack time in milliseconds + {efo_idecay, 0.0, 20000.0}, // envelope decay time in milliseconds + {efo_iexp, 0.0, 1.0}, // if 1, use exponential decay ramp (for more realistic reverb tail) + +}; + +efo_t * EFO_Params ( prc_t *pprc ) +{ + efo_t *penv; + + float threshold = Gain_To_Amplitude( dB_To_Gain(pprc->prm[efo_ithreshold]) ); + float attack = pprc->prm[efo_iattack]/1000.0; + float decay = pprc->prm[efo_idecay]/1000.0; + float fexp = pprc->prm[efo_iexp]; + bool bexp; + + // check for no gate + + if ( pprc->prm[efo_ithreshold] == 0.0 ) + threshold = 0.0; + + bexp = fexp > 0.0 ? 1 : 0; + + penv = EFO_Alloc ( threshold, attack, decay, bexp ); + return penv; +} + +inline void * EFO_VParams ( void *p ) +{ + PRC_CheckParams( (prc_t *)p, efo_rng ); + return (void *) EFO_Params ((prc_t *)p); +} + +inline void EFO_Mod ( void *p, float v ) { return; } + + +/////////////////////////////////////////// +// Chorus - lfo modulated delay +/////////////////////////////////////////// + + +#define CCRSS 64 // max number chorus' active + +struct crs_t +{ + bool fused; + + mdy_t *pmdy; // modulatable delay + lfo_t *plfo; // modulating lfo + + int lfoprev; // previous modulator value from lfo + +}; + +crs_t crss[CCRSS]; + +void CRS_Init( crs_t *pcrs ) { if (pcrs) Q_memset( pcrs, 0, sizeof (crs_t) ); }; +void CRS_Free( crs_t *pcrs ) +{ + if (pcrs) + { + MDY_Free ( pcrs->pmdy ); + LFO_Free ( pcrs->plfo ); + Q_memset( pcrs, 0, sizeof (crs_t) ); + } +} + + +void CRS_InitAll() { for (int i = 0; i < CCRSS; i++) CRS_Init( &crss[i] ); } +void CRS_FreeAll() { for (int i = 0; i < CCRSS; i++) CRS_Free( &crss[i] ); } + +// fstep is base pitch shift, ie: floating point step value, where 1.0 = +1 octave, 0.5 = -1 octave +// lfotype is LFO_SIN, LFO_RND, LFO_TRI etc (LFO_RND for chorus, LFO_SIN for flange) +// fHz is modulation frequency in Hz +// depth is modulation depth, 0-1.0 +// mix is mix of chorus and clean signal + +#define CRS_DELAYMAX 100 // max milliseconds of sweepable delay +#define CRS_RAMPTIME 5 // milliseconds to ramp between new delay values + +crs_t * CRS_Alloc( int lfotype, float fHz, float fdepth, float mix ) +{ + + int i; + crs_t *pcrs; + dly_t *pdly; + mdy_t *pmdy; + lfo_t *plfo; + float ramptime; + int D; + + // find free chorus slot + + for ( i = 0; i < CCRSS; i++ ) + { + if ( !crss[i].fused ) + break; + } + + if ( i == CCRSS ) + { + DevMsg ("DSP: Warning, failed to allocate chorus.\n" ); + return NULL; + } + + pcrs = &crss[i]; + + CRS_Init ( pcrs ); + + D = fdepth * MSEC_TO_SAMPS(CRS_DELAYMAX); // sweep from 0 - n milliseconds + + ramptime = (float) CRS_RAMPTIME / 1000.0; // # milliseconds to ramp between new values + + pdly = DLY_Alloc ( D, 0, 1, DLY_LINEAR ); + + pmdy = MDY_Alloc ( pdly, ramptime, 0.0, 0.0, mix ); + + plfo = LFO_Alloc ( lfotype, fHz, false, 1.0 ); + + if ( !plfo || !pmdy ) + { + LFO_Free ( plfo ); + MDY_Free ( pmdy ); + DevMsg ("DSP: Warning, failed to allocate lfo or mdy for chorus.\n" ); + return NULL; + } + + pcrs->pmdy = pmdy; + pcrs->plfo = plfo; + pcrs->fused = true; + + return pcrs; +} + +// return next chorused sample (modulated delay) mixed with input sample + +inline int CRS_GetNext( crs_t *pcrs, int x ) +{ + int l; + int y; + + // get current mod delay value + + y = MDY_GetNext ( pcrs->pmdy, x ); + + // get next lfo value for modulation + // note: lfo must return 0 as first value + + l = LFO_GetNext ( pcrs->plfo, x ); + + // if modulator has changed, change mdy + + if ( l != pcrs->lfoprev ) + { + // calculate new tap starts at D) + + int D = pcrs->pmdy->pdly->D0; + int tap; + + // lfo should always output values 0 <= l <= LFOMAX + + if (l < 0) + l = 0; + + tap = D - ((l * D) >> LFOBITS); + + MDY_ChangeVal ( pcrs->pmdy, tap ); + + pcrs->lfoprev = l; + } + + return y; +} + +// batch version for performance + +inline void CRS_GetNextN( crs_t *pcrs, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = CRS_GetNext( pcrs, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = CRS_GetNext( pcrs, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = CRS_GetNext( pcrs, pb->left ); + pb++; + } + return; + } +} + +// parameter order + +typedef enum { + + crs_ilfotype, + crs_irate, + crs_idepth, + crs_imix, + + crs_cparam + +} crs_e; + + +// parameter ranges + +prm_rng_t crs_rng[] = { + + {crs_cparam, 0, 0}, // first entry is # of parameters + + {crs_ilfotype, 0, LFO_MAX}, // lfotype is LFO_SIN, LFO_RND, LFO_TRI etc (LFO_RND for chorus, LFO_SIN for flange) + {crs_irate, 0.0, 1000.0}, // rate is modulation frequency in Hz + {crs_idepth, 0.0, 1.0}, // depth is modulation depth, 0-1.0 + {crs_imix, 0.0, 1.0}, // mix is mix of chorus and clean signal + +}; + +// uses pitch, lfowav, rate, depth + +crs_t * CRS_Params ( prc_t *pprc ) +{ + crs_t *pcrs; + + pcrs = CRS_Alloc ( pprc->prm[crs_ilfotype], pprc->prm[crs_irate], pprc->prm[crs_idepth], pprc->prm[crs_imix] ); + + return pcrs; +} + +inline void * CRS_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, crs_rng ); + return (void *) CRS_Params ((prc_t *)p); +} + +inline void CRS_Mod ( void *p, float v ) { return; } + + +//////////////////////////////////////////////////// +// amplifier - modulatable gain, distortion +//////////////////////////////////////////////////// + +#define CAMPS 64 // max number amps active + +#define AMPSLEW 10 // milliseconds of slew time between gain changes + +struct amp_t +{ + bool fused; + + int gain; // amplification 0-6.0 * PMAX + int gain_max; // original gain setting + int distmix; // 0-1.0 mix of distortion with clean * PMAX + int vfeed; // 0-1.0 feedback with distortion * PMAX + int vthresh; // amplitude of clipping threshold 0..32768 + + + bool fchanging; // true if modulating to new amp value + float ramptime; // ramp 'glide' time - time in seconds to change between values + int mtime; // time in samples between amp changes. 0 implies no self-modulating + int mtimecur; // current time in samples until next amp change + int depth; // modulate amp from A to A - (A*depth) depth 0-1.0 + bool brand; // if true, use random modulation otherwise alternate btwn max/min + rmp_t rmp_interp; // interpolation ramp 0...PMAX + +}; + +amp_t amps[CAMPS]; + +void AMP_Init( amp_t *pamp ) { if (pamp) Q_memset( pamp, 0, sizeof (amp_t) ); }; +void AMP_Free( amp_t *pamp ) +{ + if (pamp) + { + Q_memset( pamp, 0, sizeof (amp_t) ); + } +} + + +void AMP_InitAll() { for (int i = 0; i < CAMPS; i++) AMP_Init( &s[i] ); } +void AMP_FreeAll() { for (int i = 0; i < CAMPS; i++) AMP_Free( &s[i] ); } + + +amp_t * AMP_Alloc( float gain, float vthresh, float distmix, float vfeed, float ramptime, float modtime, float depth, bool brand ) +{ + int i; + amp_t *pamp; + + // find free amp slot + + for ( i = 0; i < CAMPS; i++ ) + { + if ( !amps[i].fused ) + break; + } + + if ( i == CAMPS ) + { + DevMsg ("DSP: Warning, failed to allocate amp.\n" ); + return NULL; + } + + pamp = &s[i]; + + AMP_Init ( pamp ); + + pamp->fused = true; + + pamp->gain = gain * PMAX; + pamp->gain_max = gain * PMAX; + pamp->distmix = distmix * PMAX; + pamp->vfeed = vfeed * PMAX; + pamp->vthresh = vthresh * 32767.0; + + // modrate, 0.01, 200.0}, // frequency at which amplitude values change to new random value. 0 is no self-modulation + // moddepth, 0.0, 1.0}, // how much amplitude changes (decreases) from current value (0-1.0) + // modglide, 0.01, 100.0}, // glide time between mapcur and ampnew in milliseconds + + pamp->ramptime = ramptime; + pamp->mtime = SEC_TO_SAMPS(modtime); + pamp->mtimecur = pamp->mtime; + pamp->depth = depth * PMAX; + pamp->brand = brand; + + return pamp; +} + +// return next amplified sample + +inline int AMP_GetNext( amp_t *pamp, int x ) +{ + int y = x; + int d; + + // if distortion is on, add distortion, feedback + + if ( pamp->vthresh < PMAX && pamp->distmix ) + { + int vthresh = pamp->vthresh; + +/* if ( pamp->vfeed > 0.0 ) + { + // UNDONE: feedback + } +*/ + // clip distort + + d = ( y > vthresh ? vthresh : ( y < -vthresh ? -vthresh : y)); + + // mix distorted with clean (1.0 = full distortion) + + if ( pamp->distmix < PMAX ) + y = y + (((d - y) * pamp->distmix ) >> PBITS); + else + y = d; + } + + // get output for current gain value + + int xout = (y * pamp->gain) >> PBITS; + + if ( !pamp->fchanging && !pamp->mtime ) + { + // if not modulating and not self modulating, return right away + + return xout; + } + + if (pamp->fchanging) + { + // modulating... + + // get next gain value + + pamp->gain = RMP_GetNext( &pamp->rmp_interp ); // 0...next gain + + if ( RMP_HitEnd( &pamp->rmp_interp ) ) + { + // done. + + pamp->fchanging = false; + } + } + + // if self-modulating and timer has expired, get next change + + if ( pamp->mtime && !pamp->mtimecur-- ) + { + pamp->mtimecur = pamp->mtime; + + int gain_new; + int G1; + int G2 = pamp->gain_max; + + // modulate between 0 and 100% of gain_max + + G1 = pamp->gain_max - ((pamp->gain_max * pamp->depth) >> PBITS); + + if (pamp->brand) + { + gain_new = RandomInt( min(G1,G2), max(G1,G2) ); + } + else + { + // alternate between min & max + + gain_new = (pamp->gain == G1 ? G2 : G1); + } + + // set up modulation to new value + + pamp->fchanging = true; + + // init gain ramp - always hit target + + RMP_Init ( &pamp->rmp_interp, pamp->ramptime, pamp->gain, gain_new, false ); + } + + return xout; + +} + +// batch version for performance + +inline void AMP_GetNextN( amp_t *pamp, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + int count = SampleCount; + portable_samplepair_t *pb = pbuffer; + + switch (op) + { + default: + case OP_LEFT: + while (count--) + { + pb->left = AMP_GetNext( pamp, pb->left ); + pb++; + } + return; + case OP_RIGHT: + while (count--) + { + pb->right = AMP_GetNext( pamp, pb->right ); + pb++; + } + return; + case OP_LEFT_DUPLICATE: + while (count--) + { + pb->left = pb->right = AMP_GetNext( pamp, pb->left ); + pb++; + } + return; + } +} + +inline void AMP_Mod( amp_t *pamp, float v ) +{ +} + + +// parameter order + +typedef enum { + + amp_gain, + amp_vthresh, + amp_distmix, + amp_vfeed, + amp_imodrate, + amp_imoddepth, + amp_imodglide, + amp_irand, + amp_cparam + +} amp_e; + + +// parameter ranges + +prm_rng_t amp_rng[] = { + + {amp_cparam, 0, 0}, // first entry is # of parameters + + {amp_gain, 0.0, 1000.0}, // amplification + {amp_vthresh, 0.0, 1.0}, // threshold for distortion (1.0 = no distortion) + {amp_distmix, 0.0, 1.0}, // mix of clean and distortion (1.0 = full distortion, 0.0 = full clean) + {amp_vfeed, 0.0, 1.0}, // distortion feedback + + {amp_imodrate, 0.0, 200.0}, // frequency at which amplitude values change to new random value. 0 is no self-modulation + {amp_imoddepth, 0.0, 1.0}, // how much amplitude changes (decreases) from current value (0-1.0) + {amp_imodglide, 0.01, 100.0}, // glide time between mapcur and ampnew in milliseconds + {amp_irand, 0.0, 1.0}, // if 1, use random modulation otherwise alternate from max-min-max +}; + +amp_t * AMP_Params ( prc_t *pprc ) +{ + amp_t *pamp; + + float ramptime = 0.0; + float modtime = 0.0; + float depth = 0.0; + float rand = pprc->prm[amp_irand]; + bool brand; + + if (pprc->prm[amp_imodrate] > 0.0) + { + ramptime = pprc->prm[amp_imodglide] / 1000.0; // get ramp time in seconds + modtime = 1.0 / max((double)pprc->prm[amp_imodrate], 0.01); // time between modulations in seconds + depth = pprc->prm[amp_imoddepth]; // depth of modulations 0-1.0 + } + + brand = rand > 0.0 ? 1 : 0; + + pamp = AMP_Alloc ( pprc->prm[amp_gain], pprc->prm[amp_vthresh], pprc->prm[amp_distmix], pprc->prm[amp_vfeed], + ramptime, modtime, depth, brand ); + + return pamp; +} + +inline void * AMP_VParams ( void *p ) +{ + PRC_CheckParams ( (prc_t *)p, amp_rng ); + return (void *) AMP_Params ((prc_t *)p); +} + + +///////////////// +// NULL processor +///////////////// + +struct nul_t +{ + int type; +}; + +nul_t nuls[] = {{0}}; + +void NULL_Init ( nul_t *pnul ) { } +void NULL_InitAll( ) { } +void NULL_Free ( nul_t *pnul ) { } +void NULL_FreeAll ( ) { } +nul_t *NULL_Alloc ( ) { return &nuls[0]; } + +inline int NULL_GetNext ( void *p, int x) { return x; } + +inline void NULL_GetNextN( nul_t *pnul, portable_samplepair_t *pbuffer, int SampleCount, int op ) { return; } + +inline void NULL_Mod ( void *p, float v ) { return; } + +inline void * NULL_VParams ( void *p ) { return (void *) (&nuls[0]); } + +////////////////////////// +// DSP processors presets - see dsp_presets.txt +////////////////////////// + + + + +// init array of processors - first store pfnParam, pfnGetNext and pfnFree functions for type, +// then call the pfnParam function to initialize each processor + +// prcs - an array of prc structures, all with initialized params +// count - number of elements in the array + +// returns false if failed to init one or more processors + +bool PRC_InitAll( prc_t *prcs, int count ) +{ + int i; + prc_Param_t pfnParam; // allocation function - takes ptr to prc, returns ptr to specialized data struct for proc type + prc_GetNext_t pfnGetNext; // get next function + prc_GetNextN_t pfnGetNextN; // get next function, batch version + prc_Free_t pfnFree; + prc_Mod_t pfnMod; + + bool fok = true;; + + if ( count == 0 ) + count = 1; + + // set up pointers to XXX_Free, XXX_GetNext and XXX_Params functions + + for (i = 0; i < count; i++) + { + switch (prcs[i].type) + { + default: + case PRC_NULL: + pfnFree = (prc_Free_t)NULL_Free; + pfnGetNext = (prc_GetNext_t)NULL_GetNext; + pfnGetNextN = (prc_GetNextN_t)NULL_GetNextN; + pfnParam = NULL_VParams; + pfnMod = (prc_Mod_t)NULL_Mod; + break; + case PRC_DLY: + pfnFree = (prc_Free_t)DLY_Free; + pfnGetNext = (prc_GetNext_t)DLY_GetNext; + pfnGetNextN = (prc_GetNextN_t)DLY_GetNextN; + pfnParam = DLY_VParams; + pfnMod = (prc_Mod_t)DLY_Mod; + break; + case PRC_RVA: + pfnFree = (prc_Free_t)RVA_Free; + pfnGetNext = (prc_GetNext_t)RVA_GetNext; + pfnGetNextN = (prc_GetNextN_t)RVA_GetNextN; + pfnParam = RVA_VParams; + pfnMod = (prc_Mod_t)RVA_Mod; + break; + case PRC_FLT: + pfnFree = (prc_Free_t)FLT_Free; + pfnGetNext = (prc_GetNext_t)FLT_GetNext; + pfnGetNextN = (prc_GetNextN_t)FLT_GetNextN; + pfnParam = FLT_VParams; + pfnMod = (prc_Mod_t)FLT_Mod; + break; + case PRC_CRS: + pfnFree = (prc_Free_t)CRS_Free; + pfnGetNext = (prc_GetNext_t)CRS_GetNext; + pfnGetNextN = (prc_GetNextN_t)CRS_GetNextN; + pfnParam = CRS_VParams; + pfnMod = (prc_Mod_t)CRS_Mod; + break; + case PRC_PTC: + pfnFree = (prc_Free_t)PTC_Free; + pfnGetNext = (prc_GetNext_t)PTC_GetNext; + pfnGetNextN = (prc_GetNextN_t)PTC_GetNextN; + pfnParam = PTC_VParams; + pfnMod = (prc_Mod_t)PTC_Mod; + break; + case PRC_ENV: + pfnFree = (prc_Free_t)ENV_Free; + pfnGetNext = (prc_GetNext_t)ENV_GetNext; + pfnGetNextN = (prc_GetNextN_t)ENV_GetNextN; + pfnParam = ENV_VParams; + pfnMod = (prc_Mod_t)ENV_Mod; + break; + case PRC_LFO: + pfnFree = (prc_Free_t)LFO_Free; + pfnGetNext = (prc_GetNext_t)LFO_GetNext; + pfnGetNextN = (prc_GetNextN_t)LFO_GetNextN; + pfnParam = LFO_VParams; + pfnMod = (prc_Mod_t)LFO_Mod; + break; + case PRC_EFO: + pfnFree = (prc_Free_t)EFO_Free; + pfnGetNext = (prc_GetNext_t)EFO_GetNext; + pfnGetNextN = (prc_GetNextN_t)EFO_GetNextN; + pfnParam = EFO_VParams; + pfnMod = (prc_Mod_t)EFO_Mod; + break; + case PRC_MDY: + pfnFree = (prc_Free_t)MDY_Free; + pfnGetNext = (prc_GetNext_t)MDY_GetNext; + pfnGetNextN = (prc_GetNextN_t)MDY_GetNextN; + pfnParam = MDY_VParams; + pfnMod = (prc_Mod_t)MDY_Mod; + break; + case PRC_DFR: + pfnFree = (prc_Free_t)DFR_Free; + pfnGetNext = (prc_GetNext_t)DFR_GetNext; + pfnGetNextN = (prc_GetNextN_t)DFR_GetNextN; + pfnParam = DFR_VParams; + pfnMod = (prc_Mod_t)DFR_Mod; + break; + case PRC_AMP: + pfnFree = (prc_Free_t)AMP_Free; + pfnGetNext = (prc_GetNext_t)AMP_GetNext; + pfnGetNextN = (prc_GetNextN_t)AMP_GetNextN; + pfnParam = AMP_VParams; + pfnMod = (prc_Mod_t)AMP_Mod; + break; + } + + // set up function pointers + + prcs[i].pfnParam = pfnParam; + prcs[i].pfnGetNext = pfnGetNext; + prcs[i].pfnGetNextN = pfnGetNextN; + prcs[i].pfnFree = pfnFree; + prcs[i].pfnMod = pfnMod; + + // call param function, store pdata for the processor type + + prcs[i].pdata = pfnParam ( (void *) (&prcs[i]) ); + + if ( !prcs[i].pdata ) + fok = false; + } + + return fok; +} + +// free individual processor's data + +void PRC_Free ( prc_t *pprc ) +{ + if ( pprc->pfnFree && pprc->pdata ) + pprc->pfnFree ( pprc->pdata ); +} + +// free all processors for supplied array +// prcs - array of processors +// count - elements in array + +void PRC_FreeAll ( prc_t *prcs, int count ) +{ + for (int i = 0; i < count; i++) + PRC_Free( &prcs[i] ); +} + +// get next value for processor - (usually called directly by PSET_GetNext) + +inline int PRC_GetNext ( prc_t *pprc, int x ) +{ + return pprc->pfnGetNext ( pprc->pdata, x ); +} + +// automatic parameter range limiting +// force parameters between specified min/max in param_rng + +void PRC_CheckParams ( prc_t *pprc, prm_rng_t *prng ) +{ + // first entry in param_rng is # of parameters + + int cprm = prng[0].iprm; + + for (int i = 0; i < cprm; i++) + { + // if parameter is 0.0, always allow it (this is 'off' for most params) + + if ( pprc->prm[i] != 0.0 && (pprc->prm[i] > prng[i+1].hi || pprc->prm[i] < prng[i+1].lo) ) + { + DevMsg ("DSP: Warning, clamping out of range parameter.\n" ); + pprc->prm[i] = clamp (pprc->prm[i], prng[i+1].lo, prng[i+1].hi); + } + } +} + + +// DSP presets + +// A dsp preset comprises one or more dsp processors in linear, parallel or feedback configuration + +// preset configurations +// +#define PSET_SIMPLE 0 + +// x(n)--->P(0)--->y(n) + +#define PSET_LINEAR 1 + +// x(n)--->P(0)-->P(1)-->...P(m)--->y(n) + + +#define PSET_PARALLEL2 5 + +// x(n)--->P(0)-->(+)-->y(n) +// ^ +// | +// x(n)--->P(1)----- + +#define PSET_PARALLEL4 6 + +// x(n)--->P(0)-->P(1)-->(+)-->y(n) +// ^ +// | +// x(n)--->P(2)-->P(3)----- + +#define PSET_PARALLEL5 7 + +// x(n)--->P(0)-->P(1)-->(+)-->P(4)-->y(n) +// ^ +// | +// x(n)--->P(2)-->P(3)----- + +#define PSET_FEEDBACK 8 + +// x(n)-P(0)--(+)-->P(1)-->P(2)---->y(n) +// ^ | +// | v +// -----P(4)<--P(3)-- + +#define PSET_FEEDBACK3 9 + +// x(n)---(+)-->P(0)--------->y(n) +// ^ | +// | v +// -----P(2)<--P(1)-- + +#define PSET_FEEDBACK4 10 + +// x(n)---(+)-->P(0)-------->P(3)--->y(n) +// ^ | +// | v +// ---P(2)<--P(1)-- + +#define PSET_MOD 11 + +// +// x(n)------>P(1)--P(2)--P(3)--->y(n) +// ^ +// x(n)------>P(0)....: + +#define PSET_MOD2 12 + +// +// x(n)-------P(1)-->y(n) +// ^ +// x(n)-->P(0)..: + + +#define PSET_MOD3 13 + +// +// x(n)-------P(1)-->P(2)-->y(n) +// ^ +// x(n)-->P(0)..: + + +#define CPSETS 64 // max number of presets simultaneously active + +#define CPSET_PRCS 5 // max # of processors per dsp preset +#define CPSET_STATES (CPSET_PRCS+3) // # of internal states + +// NOTE: do not reorder members of pset_t - g_psettemplates relies on it!!! + +struct pset_t +{ + int type; // preset configuration type + int cprcs; // number of processors for this preset + + prc_t prcs[CPSET_PRCS]; // processor preset data + + float mix_min; // min dsp mix at close range + float mix_max; // max dsp mix at long range + float db_min; // if sndlvl of a new sound is < db_min, reduce mix_min/max by db_mixdrop + float db_mixdrop; // reduce mix_min/max by n% if sndlvl of new sound less than db_min + float duration; // if > 0, duration of preset in seconds (duration 0 = infinite) + float fade; // fade out time, exponential fade + + int csamp_duration; // duration counter # samples + + int w[CPSET_STATES]; // internal states + int fused; +}; + +pset_t psets[CPSETS]; + +pset_t *g_psettemplates = NULL; +int g_cpsettemplates = 0; + +// returns true if preset will expire after duration + +bool PSET_IsOneShot( pset_t *ppset ) +{ + return ppset->duration > 0.0; +} + +// return true if preset is no longer active - duration has expired + +bool PSET_HasExpired( pset_t *ppset ) +{ + if (!PSET_IsOneShot( ppset )) + return false; + + return ppset->csamp_duration <= 0; +} + +// if preset is oneshot, update duration counter by SampleCount samples + +void PSET_UpdateDuration( pset_t *ppset, int SampleCount ) +{ + if ( PSET_IsOneShot( ppset ) ) + { + // if oneshot preset and not expired, decrement sample count + + if (ppset->csamp_duration > 0) + ppset->csamp_duration -= SampleCount; + } +} + +// A dsp processor (prc) performs a single-sample function, such as pitch shift, delay, reverb, filter + + +// init a preset - just clear state array + +void PSET_Init( pset_t *ppset ) +{ + // clear state array + + if (ppset) + Q_memset( ppset->w, 0, sizeof (int) * (CPSET_STATES) ); +} + +// clear runtime slots + +void PSET_InitAll( void ) +{ + for (int i = 0; i < CPSETS; i++) + Q_memset( &psets[i], 0, sizeof(pset_t)); +} + +// free the preset - free all processors + +void PSET_Free( pset_t *ppset ) +{ + if (ppset) + { + // free processors + + PRC_FreeAll ( ppset->prcs, ppset->cprcs ); + + // clear + + Q_memset( ppset, 0, sizeof (pset_t)); + } +} + +void PSET_FreeAll() { for (int i = 0; i < CPSETS; i++) PSET_Free( &psets[i] ); }; + +// return preset struct, given index into preset template array +// NOTE: should not ever be more than 2 or 3 of these active simultaneously + +pset_t * PSET_Alloc ( int ipsettemplate ) +{ + pset_t *ppset; + bool fok; + + // don't excede array bounds + + if ( ipsettemplate >= g_cpsettemplates) + ipsettemplate = 0; + + // find free slot + int i = 0; + for (i = 0; i < CPSETS; i++) + { + if ( !psets[i].fused ) + break; + } + + if ( i == CPSETS ) + return NULL; + + if (das_debug.GetInt()) + { + int nSlots = 0; + for ( int j = 0; j < CPSETS; j++) + { + if ( psets[j].fused ) + nSlots++; + } + DevMsg("total preset slots used: %d \n", nSlots ); + } + + + ppset = &psets[i]; + + // clear preset + + Q_memset(ppset, 0, sizeof(pset_t)); + + // copy template into preset + + *ppset = g_psettemplates[ipsettemplate]; + + ppset->fused = true; + + // clear state array + + PSET_Init ( ppset ); + + // init all processors, set up processor function pointers + + fok = PRC_InitAll( ppset->prcs, ppset->cprcs ); + + if ( !fok ) + { + // failed to init one or more processors + Warning( "Sound DSP: preset failed to init.\n"); + PRC_FreeAll ( ppset->prcs, ppset->cprcs ); + return NULL; + } + + // if preset has duration, setup duration sample counter + + if ( PSET_IsOneShot( ppset ) ) + { + ppset->csamp_duration = SEC_TO_SAMPS( ppset->duration ); + } + + return ppset; +} + +// batch version of PSET_GetNext for linear array of processors. For performance. + +// ppset - preset array +// pbuffer - input sample data +// SampleCount - size of input buffer +// OP: OP_LEFT - process left channel in place +// OP_RIGHT - process right channel in place +// OP_LEFT_DUPLICATe - process left channel, duplicate into right + +inline void PSET_GetNextN( pset_t *ppset, portable_samplepair_t *pbuffer, int SampleCount, int op ) +{ + portable_samplepair_t *pbf = pbuffer; + prc_t *pprc; + int count = ppset->cprcs; + + switch ( ppset->type ) + { + default: + case PSET_SIMPLE: + { + // x(n)--->P(0)--->y(n) + + ppset->prcs[0].pfnGetNextN (ppset->prcs[0].pdata, pbf, SampleCount, op); + return; + } + case PSET_LINEAR: + { + + // w0 w1 w2 + // x(n)--->P(0)-->P(1)-->...P(count-1)--->y(n) + + // w0 w1 w2 w3 w4 w5 + // x(n)--->P(0)-->P(1)-->P(2)-->P(3)-->P(4)-->y(n) + + // call batch processors in sequence - no internal state for batch processing + + // point to first processor + + pprc = &ppset->prcs[0]; + + for (int i = 0; i < count; i++) + { + pprc->pfnGetNextN (pprc->pdata, pbf, SampleCount, op); + pprc++; + } + + return; + } + } +} + + +// Get next sample from this preset. called once for every sample in buffer +// ppset is pointer to preset +// x is input sample + +inline int PSET_GetNext ( pset_t *ppset, int x ) +{ + + // pset_simple and pset_linear have no internal state: + // this is REQUIRED for all presets that have a batch getnextN equivalent! + + if ( ppset->type == PSET_SIMPLE ) + { + // x(n)--->P(0)--->y(n) + + return ppset->prcs[0].pfnGetNext (ppset->prcs[0].pdata, x); + } + + prc_t *pprc; + int count = ppset->cprcs; + + if ( ppset->type == PSET_LINEAR ) + { + int y = x; + + // w0 w1 w2 + // x(n)--->P(0)-->P(1)-->...P(count-1)--->y(n) + + // w0 w1 w2 w3 w4 w5 + // x(n)--->P(0)-->P(1)-->P(2)-->P(3)-->P(4)-->y(n) + + // call processors in reverse order, from count to 1 + + //for (int i = count; i > 0; i--, pprc--) + // w[i] = pprc->pfnGetNext (pprc->pdata, w[i-1]); + + // return w[count]; + + + // point to first processor, update sequentially, no state preserved + + pprc = &ppset->prcs[0]; + + switch (count) + { + default: + case 5: + y = pprc->pfnGetNext (pprc->pdata, y); + pprc++; + case 4: + y = pprc->pfnGetNext (pprc->pdata, y); + pprc++; + case 3: + y = pprc->pfnGetNext (pprc->pdata, y); + pprc++; + case 2: + y = pprc->pfnGetNext (pprc->pdata, y); + pprc++; + case 1: + case 0: + y = pprc->pfnGetNext (pprc->pdata, y); + } + + return y; + } + + // all other preset types have internal state: + + // initialize 0'th element of state array + + int *w = ppset->w; + w[0] = x; + + switch ( ppset->type ) + { + default: + + case PSET_PARALLEL2: + { // w0 w1 w3 + // x(n)--->P(0)-->(+)-->y(n) + // ^ + // w0 w2 | + // x(n)--->P(1)----- + + pprc = &ppset->prcs[0]; + + w[3] = w[1] + w[2]; + + w[1] = pprc->pfnGetNext( pprc->pdata, w[0] ); + pprc++; + w[2] = pprc->pfnGetNext( pprc->pdata, w[0] ); + + return w[3]; + } + + case PSET_PARALLEL4: + { // w0 w1 w2 w5 + // x(n)--->P(0)-->P(1)-->(+)-->y(n) + // ^ + // w0 w3 w4 | + // x(n)--->P(2)-->P(3)----- + + + pprc = &ppset->prcs[0]; + + w[5] = w[2] + w[4]; + + w[2] = pprc[1].pfnGetNext( pprc[1].pdata, w[1] ); + w[4] = pprc[3].pfnGetNext( pprc[3].pdata, w[3] ); + + w[1] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + w[3] = pprc[2].pfnGetNext( pprc[2].pdata, w[0] ); + + return w[5]; + } + + case PSET_PARALLEL5: + { // w0 w1 w2 w5 w6 + // x(n)--->P(0)-->P(1)-->(+)--P(4)-->y(n) + // ^ + // w0 w3 w4 | + // x(n)--->P(2)-->P(3)----- + + pprc = &ppset->prcs[0]; + + w[5] = w[2] + w[4]; + + w[2] = pprc[1].pfnGetNext( pprc[1].pdata, w[1] ); + w[4] = pprc[3].pfnGetNext( pprc[3].pdata, w[3] ); + + w[1] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + w[3] = pprc[2].pfnGetNext( pprc[2].pdata, w[0] ); + + return pprc[4].pfnGetNext( pprc[4].pdata, w[5] ); + } + + case PSET_FEEDBACK: + { + // w0 w1 w2 w3 w4 w7 + // x(n)-P(0)--(+)-->P(1)-->P(2)-->---->y(n) + // ^ | + // | w6 w5 v + // -----P(4)<--P(3)-- + + pprc = &ppset->prcs[0]; + + // start with adders + + w[2] = w[1] + w[6]; + + // evaluate in reverse order + + w[6] = pprc[4].pfnGetNext( pprc[4].pdata, w[5] ); + w[5] = pprc[3].pfnGetNext( pprc[3].pdata, w[4] ); + + w[4] = pprc[2].pfnGetNext( pprc[2].pdata, w[3] ); + w[3] = pprc[1].pfnGetNext( pprc[1].pdata, w[2] ); + w[1] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + + return w[4]; + } + case PSET_FEEDBACK3: + { + // w0 w1 w2 + // x(n)---(+)-->P(0)--------->y(n) + // ^ | + // | w4 w3 v + // -----P(2)<--P(1)-- + + pprc = &ppset->prcs[0]; + + // start with adders + + w[1] = w[0] + w[4]; + + // evaluate in reverse order + + w[4] = pprc[2].pfnGetNext( pprc[2].pdata, w[3] ); + w[3] = pprc[1].pfnGetNext( pprc[1].pdata, w[2] ); + w[2] = pprc[0].pfnGetNext( pprc[0].pdata, w[1] ); + + return w[2]; + } + case PSET_FEEDBACK4: + { + // w0 w1 w2 w5 + // x(n)---(+)-->P(0)-------->P(3)--->y(n) + // ^ | + // | w4 w3 v + // ---P(2)<--P(1)-- + + pprc = &ppset->prcs[0]; + + // start with adders + + w[1] = w[0] + w[4]; + + // evaluate in reverse order + + w[5] = pprc[3].pfnGetNext( pprc[3].pdata, w[2] ); + w[4] = pprc[2].pfnGetNext( pprc[2].pdata, w[3] ); + w[3] = pprc[1].pfnGetNext( pprc[1].pdata, w[2] ); + w[2] = pprc[0].pfnGetNext( pprc[0].pdata, w[1] ); + + return w[2]; + } + case PSET_MOD: + { + // w0 w1 w3 w4 + // x(n)------>P(1)--P(2)--P(3)--->y(n) + // w0 w2 ^ + // x(n)------>P(0)....: + + pprc = &ppset->prcs[0]; + + w[4] = pprc[3].pfnGetNext( pprc[3].pdata, w[3] ); + + w[3] = pprc[2].pfnGetNext( pprc[2].pdata, w[1] ); + + // modulate processor 2 + + pprc[2].pfnMod( pprc[2].pdata, ((float)w[2] / (float)PMAX)); + + // get modulator output + + w[2] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + + w[1] = pprc[1].pfnGetNext( pprc[1].pdata, w[0] ); + + return w[4]; + } + case PSET_MOD2: + { + // w0 w2 + // x(n)---------P(1)-->y(n) + // w0 w1 ^ + // x(n)-->P(0)....: + + pprc = &ppset->prcs[0]; + + // modulate processor 1 + + pprc[1].pfnMod( pprc[1].pdata, ((float)w[1] / (float)PMAX)); + + // get modulator output + + w[1] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + + w[2] = pprc[1].pfnGetNext( pprc[1].pdata, w[0] ); + + return w[2]; + + } + case PSET_MOD3: + { + // w0 w2 w3 + // x(n)----------P(1)-->P(2)-->y(n) + // w0 w1 ^ + // x(n)-->P(0).....: + + pprc = &ppset->prcs[0]; + + w[3] = pprc[2].pfnGetNext( pprc[2].pdata, w[2] ); + + // modulate processor 1 + + pprc[1].pfnMod( pprc[1].pdata, ((float)w[1] / (float)PMAX)); + + // get modulator output + + w[1] = pprc[0].pfnGetNext( pprc[0].pdata, w[0] ); + + w[2] = pprc[1].pfnGetNext( pprc[1].pdata, w[0] ); + + return w[2]; + } + } +} + + +///////////// +// DSP system +///////////// + +// Main interface + +// Whenever the preset # changes on any of these processors, the old processor is faded out, new is faded in. +// dsp_chan is optionally set when a sound is played - a preset is sent with the start_static/dynamic sound. +// +// sound1---->dsp_chan--> -------------(+)---->dsp_water--->dsp_player--->out +// sound2---->dsp_chan--> | | +// sound3---------------> ----dsp_room--- +// | | +// --dsp_indirect- + +// dsp_room - set this cvar to a preset # to change the room dsp. room fx are more prevalent farther from player. +// use: when player moves into a new room, all sounds played in room take on its reverberant character +// dsp_water - set this cvar (once) to a preset # for serial underwater sound. +// use: when player goes under water, all sounds pass through this dsp (such as low pass filter) +// dsp_player - set this cvar to a preset # to cause all sounds to run through the effect (serial, in-line). +// use: player is deafened, player fires special weapon, player is hit by special weapon. +// dsp_facingaway- set this cvar to a preset # appropriate for sounds which are played facing away from player (weapon,voice) +// +// dsp_spatial - set by system to create modulated spatial delays for left/right/front/back ears - delay value +// modulates by distance to nearest l/r surface in world + +// Dsp presets + + +ConVar dsp_room ("dsp_room", "0", FCVAR_DEMO ); // room dsp preset - sounds more distant from player (1ch) +ConVar dsp_water ("dsp_water", "14", FCVAR_DEMO ); // "14" underwater dsp preset - sound when underwater (1-2ch) +ConVar dsp_player ("dsp_player", "0", FCVAR_DEMO | FCVAR_SERVER_CAN_EXECUTE ); // dsp on player - sound when player hit by special device (1-2ch) +ConVar dsp_facingaway ("dsp_facingaway", "0", FCVAR_DEMO ); // "30" sounds that face away from player (weapons, voice) (1-4ch) +ConVar dsp_speaker ("dsp_speaker", "50", FCVAR_DEMO ); // "50" small distorted speaker sound (1ch) +ConVar dsp_spatial ("dsp_spatial", "40", FCVAR_DEMO ); // spatial delays for l/r front/rear ears +ConVar dsp_automatic ("dsp_automatic", "0", FCVAR_DEMO ); // automatic room type detection. if non zero, replaces dsp_room + +int ipset_room_prev; +int ipset_water_prev; +int ipset_player_prev; +int ipset_facingaway_prev; +int ipset_speaker_prev; +int ipset_spatial_prev; +int ipset_automatic_prev; + +// legacy room_type support + +ConVar dsp_room_type ( "room_type", "0", FCVAR_DEMO ); +int ipset_room_typeprev; + + +// DSP processors + +int idsp_room; +int idsp_water; +int idsp_player; +int idsp_facingaway; +int idsp_speaker; +int idsp_spatial; +int idsp_automatic; + +ConVar dsp_off ("dsp_off", "0", FCVAR_CHEAT | FCVAR_ALLOWED_IN_COMPETITIVE ); // set to 1 to disable all dsp processing +ConVar dsp_slow_cpu ("dsp_slow_cpu", "0", FCVAR_ARCHIVE|FCVAR_DEMO ); // set to 1 if cpu bound - ie: does not process dsp_room fx +ConVar snd_profile ("snd_profile", "0", FCVAR_DEMO ); // 1 - profile dsp, 2 - mix, 3 - load sound, 4 - all sound +ConVar dsp_volume ("dsp_volume", "1.0", FCVAR_ARCHIVE|FCVAR_DEMO ); // 0.0 - 2.0; master dsp volume control +ConVar dsp_vol_5ch ("dsp_vol_5ch", "0.5", FCVAR_DEMO ); // 0.0 - 1.0; attenuate master dsp volume for 5ch surround +ConVar dsp_vol_4ch ("dsp_vol_4ch", "0.5", FCVAR_DEMO ); // 0.0 - 1.0; attenuate master dsp volume for 4ch surround +ConVar dsp_vol_2ch ("dsp_vol_2ch", "1.0", FCVAR_DEMO ); // 0.0 - 1.0; attenuate master dsp volume for 2ch surround + +ConVar dsp_enhance_stereo("dsp_enhance_stereo", "0", FCVAR_ARCHIVE ); // 1) use dsp_spatial delays on all reverb channels + +// DSP preset executor + +#define CDSPS 32 // max number dsp executors active +#define DSPCHANMAX 5 // max number of channels dsp can process (allocs a separte processor for each chan) + +struct dsp_t +{ + bool fused; + int cchan; // 1-5 channels, ie: mono, FrontLeft, FrontRight, RearLeft, RearRight, FrontCenter + + pset_t *ppset[DSPCHANMAX]; // current preset (1-5 channels) + int ipset; // current ipreset + + pset_t *ppsetprev[DSPCHANMAX]; // previous preset (1-5 channels) + int ipsetprev; // previous ipreset + + float xfade; // crossfade time between previous preset and new + float xfade_default; // default xfade value, set in DSP_Alloc + bool bexpfade; // true if exponential crossfade + + int ipsetsav_oneshot; // previous preset before one-shot preset was set + + rmp_t xramp; // crossfade ramp +}; + +dsp_t dsps[CDSPS]; + +void DSP_Init( int idsp ) +{ + dsp_t *pdsp; + + Assert( idsp < CDSPS ); + + if (idsp < 0 || idsp >= CDSPS) + return; + + pdsp = &dsps[idsp]; + + Q_memset( pdsp, 0, sizeof (dsp_t) ); +} + +void DSP_Free( int idsp ) +{ + dsp_t *pdsp; + + Assert( idsp < CDSPS ); + + if (idsp < 0 || idsp >= CDSPS) + return; + + pdsp = &dsps[idsp]; + + for (int i = 0; i < pdsp->cchan; i++) + { + if ( pdsp->ppset[i] ) + PSET_Free( pdsp->ppset[i] ); + + if ( pdsp->ppsetprev[i] ) + PSET_Free( pdsp->ppsetprev[i] ); + } + + Q_memset( pdsp, 0, sizeof (dsp_t) ); +} + +// Init all dsp processors - called once, during engine startup + +void DSP_InitAll ( bool bLoadPresetFile ) +{ + // only load template file on engine startup + + if ( bLoadPresetFile ) + DSP_LoadPresetFile(); + + // order is important, don't rearange. + + FLT_InitAll(); + DLY_InitAll(); + RVA_InitAll(); + LFOWAV_InitAll(); + LFO_InitAll(); + + CRS_InitAll(); + PTC_InitAll(); + ENV_InitAll(); + EFO_InitAll(); + MDY_InitAll(); + AMP_InitAll(); + + PSET_InitAll(); + + for (int idsp = 0; idsp < CDSPS; idsp++) + DSP_Init( idsp ); +} + +// free all resources associated with dsp - called once, during engine shutdown + +void DSP_FreeAll (void) +{ + // order is important, don't rearange. + + for (int idsp = 0; idsp < CDSPS; idsp++) + DSP_Free( idsp ); + + AMP_FreeAll(); + MDY_FreeAll(); + EFO_FreeAll(); + ENV_FreeAll(); + PTC_FreeAll(); + CRS_FreeAll(); + + LFO_FreeAll(); + LFOWAV_FreeAll(); + RVA_FreeAll(); + DLY_FreeAll(); + FLT_FreeAll(); +} + + +// allocate a new dsp processor chain, kill the old processor. Called during dsp init only. +// ipset is new preset +// xfade is crossfade time when switching between presets (milliseconds) +// cchan is how many simultaneous preset channels to allocate (1-4) +// return index to new dsp + +int DSP_Alloc( int ipset, float xfade, int cchan ) +{ + dsp_t *pdsp; + int i; + int idsp; + int cchans = clamp( cchan, 1, DSPCHANMAX); + + // find free slot + + for ( idsp = 0; idsp < CDSPS; idsp++ ) + { + if ( !dsps[idsp].fused ) + break; + } + + if ( idsp >= CDSPS ) + return -1; + + pdsp = &dsps[idsp]; + + DSP_Init ( idsp ); + + pdsp->fused = true; + + pdsp->cchan = cchans; + + // allocate a preset processor for each channel + + pdsp->ipset = ipset; + pdsp->ipsetprev = 0; + pdsp->ipsetsav_oneshot = 0; + + for (i = 0; i < pdsp->cchan; i++) + { + pdsp->ppset[i] = PSET_Alloc ( ipset ); + pdsp->ppsetprev[i] = NULL; + } + + // set up crossfade time in seconds + + pdsp->xfade = xfade / 1000.0; + pdsp->xfade_default = pdsp->xfade; + + RMP_SetEnd(&pdsp->xramp); + + return idsp; +} + +// call modulation function of specified processor within dsp preset + +// idsp - dsp preset +// channel - channel 1-5 (l,r,rl,rr,fc) +// iproc - which processor to change (normally 0) +// value - new parameter value for processor + +// NOTE: routine returns with no result or error if any parameter is invalid. + +void DSP_ChangePresetValue( int idsp, int channel, int iproc, float value ) +{ + + dsp_t *pdsp; + pset_t *ppset; // preset + prc_Mod_t pfnMod; // modulation function + + if (idsp < 0 || idsp >= CDSPS) + return; + + if (channel >= DSPCHANMAX) + return; + + if (iproc >= CPSET_PRCS) + return; + + // get ptr to processor preset + + pdsp = &dsps[idsp]; + + // assert that this dsp processor has enough separate channels + + Assert(channel <= pdsp->cchan); + + ppset = pdsp->ppset[channel]; + + if (!ppset) + return; + + // get ptr to modulation function + + pfnMod = ppset->prcs[iproc].pfnMod; + + if (!pfnMod) + return; + + // call modulation function with new value + + pfnMod (ppset->prcs[iproc].pdata, value); +} + + +#define DSP_AUTOMATIC 1 // corresponds to Generic preset + +// if dsp_room == DSP_AUTOMATIC, then use dsp_automatic value for dsp +// any subsequent reset of dsp_room will disable automatic room detection. + +// return true if automatic room detection is enabled + +bool DSP_CheckDspAutoEnabled( void ) +{ + return (dsp_room.GetInt() == DSP_AUTOMATIC); +} + +// set dsp_automatic preset, used in place of dsp_room when automatic room detection enabled + +void DSP_SetDspAuto( int dsp_preset ) +{ + // set dsp_preset into dsp_automatic + + dsp_automatic.SetValue( dsp_preset ); +} + +// wrapper on dsp_room GetInt so that dsp_automatic can override + +int dsp_room_GetInt ( void ) +{ + // if dsp_automatic is not enabled, get room + + if (! DSP_CheckDspAutoEnabled()) + return dsp_room.GetInt(); + + // automatic room detection is on, get dsp_automatic instead of dsp_room + + return dsp_automatic.GetInt(); +} + +// wrapper on idsp_room preset so that idsp_automatic can override + +int Get_idsp_room ( void ) +{ + + // if dsp_automatic is not enabled, get room + + if ( !DSP_CheckDspAutoEnabled()) + return idsp_room; + + // automatic room detection is on, return dsp_automatic preset instead of dsp_room preset + + return idsp_automatic; +} + + +// free previous preset if not 0 + +inline void DSP_FreePrevPreset( dsp_t *pdsp ) +{ + // free previous presets if non-null - ie: rapid change of preset just kills old without xfade + + if ( pdsp->ipsetprev ) + { + for (int i = 0; i < pdsp->cchan; i++) + { + if ( pdsp->ppsetprev[i] ) + { + PSET_Free( pdsp->ppsetprev[i] ); + pdsp->ppsetprev[i] = NULL; + } + } + + pdsp->ipsetprev = 0; + } + +} + +extern ConVar dsp_mix_min; +extern ConVar dsp_mix_max; +extern ConVar dsp_db_min; +extern ConVar dsp_db_mixdrop; + +// alloc new preset if different from current +// xfade from prev to new preset +// free previous preset, copy current into previous, set up xfade from previous to new + +void DSP_SetPreset( int idsp, int ipsetnew) +{ + dsp_t *pdsp; + pset_t *ppsetnew[DSPCHANMAX]; + + Assert (idsp >= 0 && idsp < CDSPS); + + pdsp = &dsps[idsp]; + + // validate new preset range + + if ( ipsetnew >= g_cpsettemplates || ipsetnew < 0 ) + return; + + // ignore if new preset is same as current preset + + if ( ipsetnew == pdsp->ipset ) + return; + + // alloc new presets (each channel is a duplicate preset) + + Assert (pdsp->cchan <= DSPCHANMAX); + + for (int i = 0; i < pdsp->cchan; i++) + { + ppsetnew[i] = PSET_Alloc ( ipsetnew ); + if ( !ppsetnew[i] ) + { + DevMsg("WARNING: DSP preset failed to allocate.\n"); + return; + } + } + + Assert (pdsp); + + // free PREVIOUS previous preset if not 0 + + DSP_FreePrevPreset( pdsp ); + + for (int i = 0; i < pdsp->cchan; i++) + { + // current becomes previous + + pdsp->ppsetprev[i] = pdsp->ppset[i]; + + // new becomes current + + pdsp->ppset[i] = ppsetnew[i]; + } + + pdsp->ipsetprev = pdsp->ipset; + pdsp->ipset = ipsetnew; + + if ( idsp == idsp_room || idsp == idsp_automatic ) + { + // set up new dsp mix min & max, db_min & db_drop params so that new channels get new mix values + + // NOTE: only new sounds will get the new mix min/max values set in their dspmix param + // NOTE: so - no crossfade is needed between dspmix and dspmix prev, but this also means + // NOTE: that currently playing ambients will not see changes to dspmix at all. + + float mix_min = pdsp->ppset[0]->mix_min; + float mix_max = pdsp->ppset[0]->mix_max; + float db_min = pdsp->ppset[0]->db_min; + float db_mixdrop = pdsp->ppset[0]->db_mixdrop; + + dsp_mix_min.SetValue( mix_min ); + dsp_mix_max.SetValue( mix_max ); + dsp_db_min.SetValue( db_min ); + dsp_db_mixdrop.SetValue( db_mixdrop ); + } + + RMP_SetEnd( &pdsp->xramp ); + + // make sure previous dsp preset has data + + Assert (pdsp->ppsetprev[0]); + + // shouldn't be crossfading if current dsp preset == previous dsp preset + + Assert (pdsp->ipset != pdsp->ipsetprev); + + // if new preset is one-shot, keep previous preset to restore when one-shot times out + // but: don't restore previous one-shots! + + pdsp->ipsetsav_oneshot = 0; + + if ( PSET_IsOneShot( pdsp->ppset[0] ) && !PSET_IsOneShot( pdsp->ppsetprev[0] ) ) + pdsp->ipsetsav_oneshot = pdsp->ipsetprev; + + // get new xfade time from previous preset (ie: fade out time). if 0 use default. if < 0, use exponential xfade + + if ( fabs(pdsp->ppsetprev[0]->fade) > 0.0 ) + { + pdsp->xfade = fabs(pdsp->ppsetprev[0]->fade); + pdsp->bexpfade = pdsp->ppsetprev[0]->fade < 0 ? 1 : 0; + } + else + { + // no previous preset - use defauts, set in DSP_Alloc + + pdsp->xfade = pdsp->xfade_default; + pdsp->bexpfade = false; + } + + RMP_Init( &(pdsp->xramp), pdsp->xfade, 0, PMAX, false ); +} + + +#define DSP_AUTO_BASE 60 // presets 60-100 in g_psettemplates are reserved as autocreated presets +#define DSP_CAUTO_PRESETS 40 // must be same as DAS_CNODES!!! + +// construct a dsp preset based on provided parameters, +// preset is constructed within g_psettemplates[] array. +// return preset # + +// parameter batch + +struct auto_params_t +{ + // passed in params + + bool bskyabove; // true if sky is mostly above player + int width; // max width of room in inches + int length; // max length of room in inches (length always > width) + int height; // max height of room in inches + float fdiffusion; // diffusion of room 0..1.0 + float freflectivity; // average reflectivity of all surfaces in room 0..1.0 + float surface_refl[6]; // reflectivity for left,right,front,back,ceiling,floor surfaces 0.0 for open surface (sky or no hit) + + // derived params + + int shape; // ADSP_ROOM, etc 0...4 + int size; // ADSP_SIZE_SMALL, etc 0...3 + int len; // ADSP_LENGTH_SHORT, etc 0...3 + int wid; // ADSP_WIDTH_NARROW, etc 0...3 + int ht; // ADSP_HEIGHT_LOW, etc 0...3 + int reflectivity; // ADSP_DULL, etc 0..3 + int diffusion; // ADSP_EMPTY, etc 0...3 +}; + + +// select type 1..5 based on params + // 1:simple reverb + // 2:diffusor + reverb + // 3:diffusor + delay + reverb + // 4:simple delay + // 5:diffusor + delay + +#define AROOM_SMALL (10.0 * 12.0) // small room +#define AROOM_MEDIUM (20.0 * 12.0) // medium room +#define AROOM_LARGE (40.0 * 12.0) // large room +#define AROOM_HUGE (100.0 * 12.0) // huge room +#define AROOM_GIGANTIC (200.0 * 12.0) // gigantic room + +#define AROOM_DUCT_WIDTH (4.0 * 12.0) // max width for duct +#define AROOM_DUCT_HEIGHT (6.0 * 12.0) + +#define AROOM_HALL_WIDTH (8.0 * 12.0) // max width for hall +#define AROOM_HALL_HEIGHT (16.0 * 12.0) // max height for hall + +#define AROOM_TUNNEL_WIDTH (20.0 * 12.0) // max width for tunnel +#define AROOM_TUNNEL_HEIGHT (30.0 * 12.0) // max height for tunnel + +#define AROOM_STREET_WIDTH (12.0 * 12.0) // min width for street + +#define AROOM_SHORT_LENGTH (12.0 * 12.0) // max length for short hall +#define AROOM_MEDIUM_LENGTH (24.0 * 12.0) // min length for medium hall +#define AROOM_LONG_LENGTH (48.0 * 12.0) // min length for long hall +#define AROOM_VLONG_LENGTH (96.0 * 12.0) // min length for very long hall +#define AROOM_XLONG_LENGTH (192.0 * 12.0) // min length for huge hall + +#define AROOM_LOW_HEIGHT (4.0 * 12.0) // short ceiling +#define AROOM_MEDIUM_HEIGHT (128) // medium ceiling +#define AROOM_TALL_HEIGHT (18.0 * 12.0) // tall ceiling +#define AROOM_VTALL_HEIGHT (32.0 * 12.0) // very tall ceiling +#define AROOM_XTALL_HEIGHT (64.0 * 12.0) // huge tall ceiling + +#define AROOM_NARROW_WIDTH (6.0 * 12.0) // narrow width +#define AROOM_MEDIUM_WIDTH (12.0 * 12.0) // medium width +#define AROOM_WIDE_WIDTH (24.0 * 12.0) // wide width +#define AROOM_VWIDE_WIDTH (48.0 * 12.0) // very wide +#define AROOM_XWIDE_WIDTH (96.0 * 12.0) // huge width + +#define BETWEEN(a,b,c) ( ((a) > (b)) && ((a) <= (c)) ) + +#define ADSP_IsShaft(pa) (pa->height > (3.0 * pa->length)) +#define ADSP_IsRoom(pa) (pa->length <= (2.5 * pa->width)) +#define ADSP_IsHall(pa) ((pa->length > (2.5 * pa->width)) && (BETWEEN(pa->width, AROOM_DUCT_WIDTH, AROOM_HALL_WIDTH))) +#define ADSP_IsTunnel(pa) ((pa->length > (4.0 * pa->width)) && (pa->width > AROOM_HALL_WIDTH)) +#define ADSP_IsDuct(pa) ((pa->length > (4.0 * pa->width)) && (pa->width <= AROOM_DUCT_WIDTH)) + +#define ADSP_IsCourtyard(pa) (pa->length <= (2.5 * pa->width)) +#define ADSP_IsAlley(pa) ((pa->length > (2.5 * pa->width)) && (pa->width <= AROOM_STREET_WIDTH)) +#define ADSP_IsStreet(pa) ((pa->length > (2.5 * pa->width)) && (pa->width > AROOM_STREET_WIDTH)) + +#define ADSP_IsSmallRoom(pa) (pa->length <= AROOM_SMALL) +#define ADSP_IsMediumRoom(pa) ((BETWEEN(pa->length, AROOM_SMALL, AROOM_MEDIUM)) ) // && (BETWEEN(pa->width, AROOM_SMALL, AROOM_MEDIUM))) +#define ADSP_IsLargeRoom(pa) (BETWEEN(pa->length, AROOM_MEDIUM, AROOM_LARGE) ) // && BETWEEN(pa->width, AROOM_MEDIUM, AROOM_LARGE)) +#define ADSP_IsHugeRoom(pa) (BETWEEN(pa->length, AROOM_LARGE, AROOM_HUGE) ) // && BETWEEN(pa->width, AROOM_LARGE, AROOM_HUGE)) +#define ADSP_IsGiganticRoom(pa) ((pa->length > AROOM_HUGE) ) // && (pa->width > AROOM_HUGE)) + +#define ADSP_IsShortLength(pa) (pa->length <= AROOM_SHORT_LENGTH) +#define ADSP_IsMediumLength(pa) (BETWEEN(pa->length, AROOM_SHORT_LENGTH, AROOM_MEDIUM_LENGTH)) +#define ADSP_IsLongLength(pa) (BETWEEN(pa->length, AROOM_MEDIUM_LENGTH, AROOM_LONG_LENGTH)) +#define ADSP_IsVLongLength(pa) (BETWEEN(pa->length, AROOM_LONG_LENGTH, AROOM_VLONG_LENGTH)) +#define ADSP_IsXLongLength(pa) (pa->length > AROOM_VLONG_LENGTH) + +#define ADSP_IsLowHeight(pa) (pa->height <= AROOM_LOW_HEIGHT) +#define ADSP_IsMediumHeight(pa) (BETWEEN(pa->height, AROOM_LOW_HEIGHT, AROOM_MEDIUM_HEIGHT)) +#define ADSP_IsTallHeight(pa) (BETWEEN(pa->height, AROOM_MEDIUM_HEIGHT, AROOM_TALL_HEIGHT)) +#define ADSP_IsVTallHeight(pa) (BETWEEN(pa->height, AROOM_TALL_HEIGHT, AROOM_VTALL_HEIGHT)) +#define ADSP_IsXTallHeight(pa) (pa->height > AROOM_VTALL_HEIGHT) + +#define ADSP_IsNarrowWidth(pa) (pa->width <= AROOM_NARROW_WIDTH) +#define ADSP_IsMediumWidth(pa) (BETWEEN(pa->width, AROOM_NARROW_WIDTH, AROOM_MEDIUM_WIDTH)) +#define ADSP_IsWideWidth(pa) (BETWEEN(pa->width, AROOM_MEDIUM_WIDTH, AROOM_WIDE_WIDTH)) +#define ADSP_IsVWideWidth(pa) (BETWEEN(pa->width, AROOM_WIDE_WIDTH, AROOM_VWIDE_WIDTH)) +#define ADSP_IsXWideWidth(pa) (pa->width > AROOM_VWIDE_WIDTH) + +#define ADSP_IsInside(pa) (!(pa->bskyabove)) + +// room diffusion + +#define ADSP_EMPTY 0 +#define ADSP_SPARSE 1 +#define ADSP_CLUTTERED 2 +#define ADSP_FULL 3 +#define ADSP_DIFFUSION_MAX 4 + +#define AROOM_DIF_EMPTY 0.01 // 1% of space by volume is other objects +#define AROOM_DIF_SPARSE 0.1 // 10% " +#define AROOM_DIF_CLUTTERED 0.3 // 30% " +#define AROOM_DIF_FULL 0.5 // 50% " + +#define ADSP_IsEmpty(pa) (pa->fdiffusion <= AROOM_DIF_EMPTY) +#define ADSP_IsSparse(pa) (BETWEEN(pa->fdiffusion, AROOM_DIF_EMPTY, AROOM_DIF_SPARSE)) +#define ADSP_IsCluttered(pa) (BETWEEN(pa->fdiffusion, AROOM_DIF_SPARSE, AROOM_DIF_CLUTTERED)) +#define ADSP_IsFull(pa) (pa->fdiffusion > AROOM_DIF_CLUTTERED) + +#define ADSP_IsDiffuse(pa) (pa->diffusion > ADSP_SPARSE) + +// room acoustic reflectivity + + // tile 0.3 * 3.3 = 0.99 + // metal 0.25 * 3.3 = 0.83 + // concrete,rock,brick,glass,gravel 0.2 * 3.3 = 0.66 + // metal panel/vent, wood, water 0.1 * 3.3 = 0.33 + // carpet,sand,snow,dirt 0.01 * 3.3 = 0.03 + +#define ADSP_DULL 0 +#define ADSP_FLAT 1 +#define ADSP_REFLECTIVE 2 +#define ADSP_BRIGHT 3 +#define ADSP_REFLECTIVITY_MAX 4 + +#define AROOM_REF_DULL 0.04 +#define AROOM_REF_FLAT 0.50 +#define AROOM_REF_REFLECTIVE 0.80 +#define AROOM_REF_BRIGHT 0.99 + +#define ADSP_IsDull(pa) (pa->freflectivity <= AROOM_REF_DULL) +#define ADSP_IsFlat(pa) (BETWEEN(pa->freflectivity, AROOM_REF_DULL, AROOM_REF_FLAT)) +#define ADSP_IsReflective(pa) (BETWEEN(pa->freflectivity, AROOM_REF_FLAT, AROOM_REF_REFLECTIVE)) +#define ADSP_IsBright(pa) (pa->freflectivity > AROOM_REF_REFLECTIVE) + +#define ADSP_IsRefl(pa) (pa->reflectivity > ADSP_FLAT) + +// room shapes + +#define ADSP_ROOM 0 +#define ADSP_DUCT 1 +#define ADSP_HALL 2 +#define ADSP_TUNNEL 3 +#define ADSP_STREET 4 +#define ADSP_ALLEY 5 +#define ADSP_COURTYARD 6 +#define ADSP_OPEN_SPACE 7 // NOTE: 7..10 must remain in order !!! +#define ADSP_OPEN_WALL 8 +#define ADSP_OPEN_STREET 9 +#define ADSP_OPEN_COURTYARD 10 + +// room sizes + +#define ADSP_SIZE_SMALL 0 // NOTE: must remain 0..4!!! +#define ADSP_SIZE_MEDIUM 1 +#define ADSP_SIZE_LARGE 2 +#define ADSP_SIZE_HUGE 3 +#define ADSP_SIZE_GIGANTIC 4 +#define ADSP_SIZE_MAX 5 + +#define ADSP_LENGTH_SHORT 0 +#define ADSP_LENGTH_MEDIUM 1 +#define ADSP_LENGTH_LONG 2 +#define ADSP_LENGTH_VLONG 3 +#define ADSP_LENGTH_XLONG 4 +#define ADSP_LENGTH_MAX 5 + +#define ADSP_WIDTH_NARROW 0 +#define ADSP_WIDTH_MEDIUM 1 +#define ADSP_WIDTH_WIDE 2 +#define ADSP_WIDTH_VWIDE 3 +#define ADSP_WIDTH_XWIDE 4 +#define ADSP_WIDTH_MAX 5 + +#define ADSP_HEIGHT_LOW 0 +#define ADSP_HEIGTH_MEDIUM 1 +#define ADSP_HEIGHT_TALL 2 +#define ADSP_HEIGHT_VTALL 3 +#define ADSP_HEIGHT_XTALL 4 +#define ADSP_HEIGHT_MAX 5 + + +// convert numeric size params to #defined size params + +void ADSP_GetSize( auto_params_t *pa ) +{ + pa->size = ((ADSP_IsSmallRoom(pa) ? 1 : 0) * ADSP_SIZE_SMALL) + + ((ADSP_IsMediumRoom(pa) ? 1 : 0) * ADSP_SIZE_MEDIUM) + + ((ADSP_IsLargeRoom(pa) ? 1 : 0) * ADSP_SIZE_LARGE) + + ((ADSP_IsHugeRoom(pa) ? 1 : 0) * ADSP_SIZE_HUGE) + + ((ADSP_IsGiganticRoom(pa) ? 1 : 0) * ADSP_SIZE_GIGANTIC); + + pa->len = ((ADSP_IsShortLength(pa) ? 1 : 0) * ADSP_LENGTH_SHORT) + + ((ADSP_IsMediumLength(pa) ? 1 : 0) * ADSP_LENGTH_MEDIUM) + + ((ADSP_IsLongLength(pa) ? 1 : 0) * ADSP_LENGTH_LONG) + + ((ADSP_IsVLongLength(pa) ? 1 : 0) * ADSP_LENGTH_VLONG) + + ((ADSP_IsXLongLength(pa) ? 1 : 0) * ADSP_LENGTH_XLONG); + + pa->wid = ((ADSP_IsNarrowWidth(pa) ? 1 : 0) * ADSP_WIDTH_NARROW) + + ((ADSP_IsMediumWidth(pa) ? 1 : 0) * ADSP_WIDTH_MEDIUM) + + ((ADSP_IsWideWidth(pa) ? 1 : 0) * ADSP_WIDTH_WIDE) + + ((ADSP_IsVWideWidth(pa) ? 1 : 0) * ADSP_WIDTH_VWIDE) + + ((ADSP_IsXWideWidth(pa) ? 1 : 0) * ADSP_WIDTH_XWIDE); + + pa->ht = ((ADSP_IsLowHeight(pa) ? 1 : 0) * ADSP_HEIGHT_LOW) + + ((ADSP_IsMediumHeight(pa) ? 1 : 0) * ADSP_HEIGTH_MEDIUM) + + ((ADSP_IsTallHeight(pa) ? 1 : 0) * ADSP_HEIGHT_TALL) + + ((ADSP_IsVTallHeight(pa) ? 1 : 0) * ADSP_HEIGHT_VTALL) + + ((ADSP_IsXTallHeight(pa) ? 1 : 0) * ADSP_HEIGHT_XTALL); + + pa->reflectivity = + ((ADSP_IsDull(pa) ? 1 : 0) * ADSP_DULL) + + ((ADSP_IsFlat(pa) ? 1 : 0) * ADSP_FLAT) + + ((ADSP_IsReflective(pa) ? 1 : 0) * ADSP_REFLECTIVE) + + ((ADSP_IsBright(pa) ? 1 : 0) * ADSP_BRIGHT); + + pa->diffusion = + ((ADSP_IsEmpty(pa) ? 1 : 0) * ADSP_EMPTY) + + ((ADSP_IsSparse(pa) ? 1 : 0) * ADSP_SPARSE) + + ((ADSP_IsCluttered(pa) ? 1 : 0) * ADSP_CLUTTERED) + + ((ADSP_IsFull(pa) ? 1 : 0) * ADSP_FULL); + + Assert(pa->size < ADSP_SIZE_MAX); + Assert(pa->len < ADSP_LENGTH_MAX); + Assert(pa->wid < ADSP_WIDTH_MAX); + Assert(pa->ht < ADSP_HEIGHT_MAX); + Assert(pa->reflectivity < ADSP_REFLECTIVITY_MAX); + Assert(pa->diffusion < ADSP_DIFFUSION_MAX); + + if ( pa->shape != ADSP_COURTYARD && pa->shape != ADSP_OPEN_COURTYARD ) + { + // fix up size for streets, alleys, halls, ducts, tunnelsy + + if (pa->shape == ADSP_STREET || pa->shape == ADSP_ALLEY ) + pa->size = pa->wid; + else + pa->size = (pa->len + pa->wid) / 2; + + } + +} + +void ADSP_GetOutsideSize( auto_params_t *pa ) +{ + ADSP_GetSize( pa ); +} + +// return # of sides that had max length or sky hits (out of 6 sides). + +int ADSP_COpenSides( auto_params_t *pa ) +{ + int count = 0; + + // only look at left,right,front,back walls - ignore floor, ceiling + + for (int i = 0; i < 4; i++) + { + if (pa->surface_refl[i] == 0.0) + count++; + } + + return count; +} + +// given auto params, return shape and size of room + +void ADSP_GetAutoShape( auto_params_t *pa ) +{ + + // INSIDE: + // shapes: duct, hall, tunnel, shaft (vertical duct, hall or tunnel) + // sizes: short->long, narrow->wide, low->tall + // shapes: room + // sizes: small->large, low->tall + + // OUTSIDE: + // shapes: street, alley + // sizes: short->long, narrow->wide + // shapes: courtyard + // sizes: small->large + + // shapes: open_space, wall, open_street, open_corner, open_courtyard + // sizes: open, narrow->wide + + bool bshaft = false; + int t; + + if (ADSP_IsInside(pa)) + { + if (ADSP_IsShaft(pa)) + { + // temp swap height and length + + bshaft = true; + t = pa->height; + pa->height = pa->length; + pa->length = t; + if (das_debug.GetInt() > 1) + DevMsg("VERTICAL SHAFT Detected \n"); + } + + // get shape + + if (ADSP_IsDuct(pa)) + { + pa->shape = ADSP_DUCT; + ADSP_GetSize( pa ); + if (das_debug.GetInt() > 1) + DevMsg("DUCT Detected \n"); + goto autoshape_exit; + } + + if (ADSP_IsHall(pa)) + { + // get size + pa->shape = ADSP_HALL; + ADSP_GetSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("HALL Detected \n"); + + goto autoshape_exit; + } + + if (ADSP_IsTunnel(pa)) + { + // get size + pa->shape = ADSP_TUNNEL; + ADSP_GetSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("TUNNEL Detected \n"); + + goto autoshape_exit; + } + + // default + // (ADSP_IsRoom(pa)) + { + // get size + pa->shape = ADSP_ROOM; + ADSP_GetSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("ROOM Detected \n"); + + goto autoshape_exit; + } + } + + // outside: + + if (ADSP_COpenSides(pa) > 0) // side hit sky, or side has max length + { + // get shape - courtyard, street, wall or open space + // 10..7 + pa->shape = ADSP_OPEN_COURTYARD - (ADSP_COpenSides(pa) - 1); + ADSP_GetOutsideSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("OPEN SIDED OUTDOOR AREA Detected \n"); + + goto autoshape_exit; + } + + // all sides closed: + + // get shape - closed street or alley or courtyard + + if (ADSP_IsCourtyard(pa)) + { + pa->shape = ADSP_COURTYARD; + ADSP_GetOutsideSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("OUTSIDE COURTYARD Detected \n"); + + goto autoshape_exit; + } + + if (ADSP_IsAlley(pa)) + { + pa->shape = ADSP_ALLEY; + ADSP_GetOutsideSize( pa ); + + if (das_debug.GetInt() > 1) + DevMsg("OUTSIDE ALLEY Detected \n"); + goto autoshape_exit; + } + + // default to 'street' if sides are closed + + // if (ADSP_IsStreet(pa)) + { + pa->shape = ADSP_STREET; + ADSP_GetOutsideSize( pa ); + if (das_debug.GetInt() > 1) + DevMsg("OUTSIDE STREET Detected \n"); + goto autoshape_exit; + } + +autoshape_exit: + + // swap height & length if needed + + if (bshaft) + { + t = pa->height; + pa->height = pa->length; + pa->length = t; + } +} + +int MapReflectivityToDLYCutoff[] = +{ + 1000, // DULL + 2000, // FLAT + 4000, // REFLECTIVE + 6000 // BRIGHT +}; + +float MapSizeToDLYFeedback[] = +{ + 0.9, // 0.6, // SMALL + 0.8, // 0.5, // MEDIUM + 0.7, // 0.4, // LARGE + 0.6, // 0.3, // HUGE + 0.5, // 0.2, // GIGANTIC +}; + +void ADSP_SetupAutoDelay( prc_t *pprc_dly, auto_params_t *pa ) +{ + // shapes: + // inside: duct, long hall, long tunnel, large room + // outside: open courtyard, street wall, space + // outside: closed courtyard, alley, street + + // size 0..4 + // len 0..3 + // wid 0..3 + // reflectivity: 0..3 + // diffusion 0..3 + + // dtype: delay type DLY_PLAIN, DLY_LOWPASS, DLY_ALLPASS + // delay: delay in milliseconds (room max size in feet) + // feedback: feedback 0-1.0 + // gain: final gain of output stage, 0-1.0 + + int size = pa->length * 2.0; + + if (pa->shape == ADSP_ALLEY || pa->shape == ADSP_STREET || pa->shape == ADSP_OPEN_STREET) + size = pa->width * 2.0; + + pprc_dly->type = PRC_DLY; + + pprc_dly->prm[dly_idtype] = DLY_LOWPASS; // delay with feedback + + pprc_dly->prm[dly_idelay] = clamp((size / 12.0), 5.0, 500.0); + + pprc_dly->prm[dly_ifeedback] = MapSizeToDLYFeedback[pa->len]; + + // reduce gain based on distance reflection travels +// float g = 1.0 - ( clamp(pprc_dly->prm[dly_idelay], 10.0, 1000.0) / (1000.0 - 10.0) ); +// pprc_dly->prm[dly_igain] = g; + + pprc_dly->prm[dly_iftype] = FLT_LP; + if (ADSP_IsInside(pa)) + pprc_dly->prm[dly_icutoff] = MapReflectivityToDLYCutoff[pa->reflectivity]; + else + pprc_dly->prm[dly_icutoff] = (int)((float)(MapReflectivityToDLYCutoff[pa->reflectivity]) * 0.75); + + pprc_dly->prm[dly_iqwidth] = 0; + + pprc_dly->prm[dly_iquality] = QUA_LO; + + float l = clamp((pa->length * 2.0 / 12.0), 14.0, 500.0); + float w = clamp((pa->width * 2.0 / 12.0), 14.0, 500.0); + + // convert to multitap delay + + pprc_dly->prm[dly_idtype] = DLY_LOWPASS_4TAP; + + pprc_dly->prm[dly_idelay] = l; + pprc_dly->prm[dly_itap1] = w; + pprc_dly->prm[dly_itap2] = l; // max(7, l * 0.7 ); + pprc_dly->prm[dly_itap3] = l; // max(7, w * 0.7 ); + + pprc_dly->prm[dly_igain] = 1.0; +} + +int MapReflectivityToRVACutoff[] = +{ + 1000, // DULL + 2000, // FLAT + 4000, // REFLECTIVE + 6000 // BRIGHT +}; + +float MapSizeToRVANumDelays[] = +{ + 3, // SMALL 3 reverbs + 6, // MEDIUM 6 reverbs + 6, // LARGE 6 reverbs + 9, // HUGE 9 reverbs + 12, // GIGANTIC 12 reverbs +}; + +float MapSizeToRVAFeedback[] = +{ + 0.75, // SMALL + 0.8, // MEDIUM + 0.9, // LARGE + 0.95, // HUGE + 0.98, // GIGANTIC +}; + +void ADSP_SetupAutoReverb( prc_t *pprc_rva, auto_params_t *pa ) +{ + // shape: hall, tunnel or room + // size 0..4 + // reflectivity: 0..3 + // diffusion 0..3 + + // size: 0-2.0 scales nominal delay parameters (18 to 47 ms * scale = delay) + // numdelays: 0-12 controls # of parallel or series delays + // decay: 0-2.0 scales feedback parameters (.7 to .9 * scale/2.0 = feedback) + // fparallel: if true, filters are built into delays, otherwise filter output only + // fmoddly: if true, all delays are modulating delays + float gain = 1.0; + + pprc_rva->type = PRC_RVA; + + pprc_rva->prm[rva_size_max] = 50.0; + pprc_rva->prm[rva_size_min] = 30.0; + + if (ADSP_IsRoom(pa)) + pprc_rva->prm[rva_inumdelays] = MapSizeToRVANumDelays[pa->size]; + else + pprc_rva->prm[rva_inumdelays] = MapSizeToRVANumDelays[pa->len]; + + pprc_rva->prm[rva_ifeedback] = 0.9; + + pprc_rva->prm[rva_icutoff] = MapReflectivityToRVACutoff[pa->reflectivity]; + + pprc_rva->prm[rva_ifparallel] = 1; + pprc_rva->prm[rva_imoddly] = ADSP_IsEmpty(pa) ? 0 : 4; + pprc_rva->prm[rva_imodrate] = 3.48; + + pprc_rva->prm[rva_iftaps] = 0; // 0.1 // use extra delay taps to increase density + + pprc_rva->prm[rva_width] = clamp( ((float)(pa->width) / 12.0), 6.0, 500.0); // in feet + pprc_rva->prm[rva_depth] = clamp( ((float)(pa->length) / 12.0), 6.0, 500.0); + pprc_rva->prm[rva_height] = clamp( ((float)(pa->height) / 12.0), 6.0, 500.0); + + // room + pprc_rva->prm[rva_fbwidth] = 0.9; // MapSizeToRVAFeedback[pa->size]; // larger size = more feedback + pprc_rva->prm[rva_fbdepth] = 0.9; // MapSizeToRVAFeedback[pa->size]; + pprc_rva->prm[rva_fbheight] = 0.5; // MapSizeToRVAFeedback[pa->size]; + + // feedback is based on size of room: + + if (ADSP_IsInside(pa)) + { + if (pa->shape == ADSP_HALL) + { + pprc_rva->prm[rva_fbwidth] = 0.7; //MapSizeToRVAFeedback[pa->wid]; + pprc_rva->prm[rva_fbdepth] = -0.5; //MapSizeToRVAFeedback[pa->len]; + pprc_rva->prm[rva_fbheight] = 0.3; //MapSizeToRVAFeedback[pa->ht]; + } + + if (pa->shape == ADSP_TUNNEL) + { + pprc_rva->prm[rva_fbwidth] = 0.9; + pprc_rva->prm[rva_fbdepth] = -0.8; // fixed pre-delay, no feedback + pprc_rva->prm[rva_fbheight] = 0.3; + } + } + else + { + if (pa->shape == ADSP_ALLEY) + { + pprc_rva->prm[rva_fbwidth] = 0.9; + pprc_rva->prm[rva_fbdepth] = -0.8; // fixed pre-delay, no feedback + pprc_rva->prm[rva_fbheight] = 0.0; + } + } + + if (!ADSP_IsInside(pa)) + pprc_rva->prm[rva_fbheight] = 0.0; + + pprc_rva->prm[rva_igain] = gain; +} + +// diffusor templates for auto create + + // size: 0-1.0 scales all delays (13ms to 41ms * scale = delay) + // numdelays: 0-4.0 controls # of series delays + // decay: 0-1.0 scales all feedback parameters + +// prctype size #dly feedback + +#if 0 +#define PRC_DFRA_S {PRC_DFR, {0.5, 2, 0.10}, NULL,NULL,NULL,NULL,NULL} // S room +#define PRC_DFRA_M {PRC_DFR, {0.75, 2, 0.12}, NULL,NULL,NULL,NULL,NULL} // M room +#define PRC_DFRA_L {PRC_DFR, {1.0, 3, 0.13}, NULL,NULL,NULL,NULL,NULL} // L room +#define PRC_DFRA_VL {PRC_DFR, {1.0, 3, 0.15}, NULL,NULL,NULL,NULL,NULL} // VL room + +prc_t g_prc_dfr_auto[] = {PRC_DFRA_S, PRC_DFRA_M, PRC_DFRA_L, PRC_DFRA_VL, PRC_DFRA_VL}; + +//$BUGBUGBUG: I think this should be sizeof(prc_t), not sizeof(pset_t)... +#define CDFRTEMPLATES (sizeof(g_prc_dfr_auto)/sizeof(pset_t)) // number of diffusor templates + +// copy diffusor template from preset list, based on room size + +void ADSP_SetupAutoDiffusor( prc_t *pprc_dfr, auto_params_t *pa ) +{ + int i = clamp(pa->size, 0, (int)CDFRTEMPLATES - 1); + + // copy diffusor preset based on size + + *pprc_dfr = g_prc_dfr_auto[i]; +} +#endif + +// return index to processor given processor type and preset +// skips N processors of similar type +// returns -1 if type not found + +int ADSP_FindProc( pset_t *ppset, int proc_type, int skip ) +{ + int skipcount = skip; + + for (int i = 0; i < ppset->cprcs; i++) + { + // look for match on processor type + + if ( ppset->prcs[i].type == proc_type ) + { + // skip first N procs of similar type, + + // return index to processor + + if (!skipcount) + return i; + + skipcount--; + } + + } + + return -1; +} + +// interpolate parameter: +// pnew - target preset +// pmin - preset with parameter with min value +// pmax - preset with parameter with max value +// proc_type - type of processor to look for ie: PRC_RVA or PRC_DLY +// skipprocs - skip n processors of type +// iparam - which parameter within processor to interpolate +// index - +// index_max: use index/index_max as interpolater between pmin param and pmax param +// if bexp is true, interpolate exponentially as (index/index_max)^2 + +// NOTE: returns with no result if processor type is not found in all presets. + +void ADSP_InterpParam( pset_t *pnew, pset_t *pmin, pset_t *pmax, int proc_type, int skipprocs, int iparam, int index, int index_max, bool bexp ) +{ + // find processor index in pnew + int iproc_new = ADSP_FindProc( pnew, proc_type, skipprocs); + int iproc_min = ADSP_FindProc( pmin, proc_type, skipprocs); + int iproc_max = ADSP_FindProc( pmax, proc_type, skipprocs); + + // make sure processor type found in all presets + + if ( iproc_new < 0 || iproc_min < 0 || iproc_max < 0 ) + return; + + float findex = (float)index/(float)index_max; + float vmin = pmin->prcs[iproc_min].prm[iparam]; + float vmax = pmax->prcs[iproc_max].prm[iparam]; + float vinterp; + + // interpolate + + if (!bexp) + vinterp = vmin + (vmax - vmin) * findex; + else + vinterp = vmin + (vmax - vmin) * findex * findex; + + pnew->prcs[iproc_new].prm[iparam] = vinterp; + + return; +} + +// directly set parameter + +void ADSP_SetParam( pset_t *pnew, int proc_type, int skipprocs, int iparam, float value ) +{ + int iproc_new = ADSP_FindProc( pnew, proc_type, skipprocs); + + if (iproc_new >= 0) + pnew->prcs[iproc_new].prm[iparam] = value; +} + +// directly set parameter if min or max is negative + +void ADSP_SetParamIfNegative( pset_t *pnew, pset_t *pmin, pset_t *pmax, int proc_type, int skipprocs, int iparam, int index, int index_max, bool bexp, float value ) +{ + // find processor index in pnew + int iproc_new = ADSP_FindProc( pnew, proc_type, skipprocs); + int iproc_min = ADSP_FindProc( pmin, proc_type, skipprocs); + int iproc_max = ADSP_FindProc( pmax, proc_type, skipprocs); + + // make sure processor type found in all presets + + if ( iproc_new < 0 || iproc_min < 0 || iproc_max < 0 ) + return; + + float vmin = pmin->prcs[iproc_min].prm[iparam]; + float vmax = pmax->prcs[iproc_max].prm[iparam]; + + if ( vmin < 0.0 || vmax < 0.0 ) + ADSP_SetParam( pnew, proc_type, skipprocs, iparam, value ); + else + ADSP_InterpParam( pnew, pmin, pmax, proc_type, skipprocs, iparam, index, index_max, bexp); + + return; +} + +// given min and max preset and auto parameters, create new preset +// NOTE: the # and type of processors making up pmin and pmax presets must be identical! + +void ADSP_InterpolatePreset( pset_t *pnew, pset_t *pmin, pset_t *pmax, auto_params_t *pa, int iskip ) +{ + int i; + + // if size > mid size, then copy basic processors from MAX preset, + // otherwise, copy from MIN preset + + if ( !iskip ) + { + // only copy on 1st call + + if ( pa->size > ADSP_SIZE_MEDIUM ) + { + *pnew = *pmax; + } + else + { + *pnew = *pmin; + } + } + + // DFR + + // interpolate all DFR params on size + + for (i = 0; i < dfr_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_DFR, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + // RVA + + // interpolate size_max, size_min, feedback, #delays, moddly, imodrate, based on ap size + + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_ifeedback, pa->size, ADSP_SIZE_MAX, 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_size_min, pa->size, ADSP_SIZE_MAX, 1); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_size_max, pa->size, ADSP_SIZE_MAX, 1); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_igain, pa->size, ADSP_SIZE_MAX, 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_inumdelays, pa->size, ADSP_SIZE_MAX , 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_imoddly, pa->size, ADSP_SIZE_MAX , 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_imodrate, pa->size, ADSP_SIZE_MAX , 0); + + // interpolate width,depth,height based on ap width length & height - exponential interpolation + // if pmin or pmax parameters are < 0, directly set value from w/l/h + + float w = clamp( ((float)(pa->width) / 12.0), 6.0, 500.0); // in feet + float l = clamp( ((float)(pa->length) / 12.0), 6.0, 500.0); + float h = clamp( ((float)(pa->height) / 12.0), 6.0, 500.0); + + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_RVA, iskip, rva_width, pa->wid, ADSP_WIDTH_MAX, 1, w); + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_RVA, iskip, rva_depth, pa->len, ADSP_LENGTH_MAX, 1, l); + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_RVA, iskip, rva_height, pa->ht, ADSP_HEIGHT_MAX, 1, h); + + // interpolate w/d/h feedback based on ap w/d/f + + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_fbwidth, pa->wid, ADSP_WIDTH_MAX , 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_fbdepth, pa->len, ADSP_LENGTH_MAX , 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_RVA, iskip, rva_fbheight, pa->ht, ADSP_HEIGHT_MAX , 0); + + // interpolate cutoff based on ap reflectivity + // NOTE: cutoff goes from max to min! ie: small bright - large dull + + ADSP_InterpParam( pnew, pmax, pmin, PRC_RVA, iskip, rva_icutoff, pa->reflectivity, ADSP_REFLECTIVITY_MAX , 0); + + // don't interpolate: fparallel, ftaps + + // DLY + + // directly set delay value from pa->length if pmin or pmax value is < 0 + + l = clamp((pa->length * 2.0 / 12.0), 14.0, 500.0); + w = clamp((pa->width * 2.0 / 12.0), 14.0, 500.0); + + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_DLY, iskip, dly_idelay, pa->len, ADSP_LENGTH_MAX, 1, l); + + // interpolate feedback, gain, based on max size (length) + + ADSP_InterpParam( pnew, pmin, pmax, PRC_DLY, iskip, dly_ifeedback, pa->len, ADSP_LENGTH_MAX , 0); + ADSP_InterpParam( pnew, pmin, pmax, PRC_DLY, iskip, dly_igain, pa->len, ADSP_LENGTH_MAX , 0); + + // directly set tap value from pa->width if pmin or pmax value is < 0 + + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_DLY, iskip, dly_itap1, pa->len, ADSP_LENGTH_MAX, 1, w); + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_DLY, iskip, dly_itap2, pa->len, ADSP_LENGTH_MAX, 1, l); + ADSP_SetParamIfNegative( pnew, pmin, pmax, PRC_DLY, iskip, dly_itap3, pa->len, ADSP_LENGTH_MAX, 1, l); + + // interpolate cutoff and qwidth based on reflectivity NOTE: this can affect gain! + // NOTE: cutoff goes from max to min! ie: small bright - large dull + + ADSP_InterpParam( pnew, pmax, pmin, PRC_DLY, iskip, dly_icutoff, pa->len, ADSP_LENGTH_MAX , 0); + ADSP_InterpParam( pnew, pmax, pmin, PRC_DLY, iskip, dly_iqwidth, pa->len, ADSP_LENGTH_MAX , 0); + + // interpolate all other parameters for all other processor types based on size + + // PRC_MDY, PRC_AMP, PRC_FLT, PTC, CRS, ENV, EFO, LFO + + for (i = 0; i < mdy_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_MDY, iskip, i, pa->len, ADSP_LENGTH_MAX , 0); + + for (i = 0; i < amp_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_AMP, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < flt_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_FLT, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < ptc_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_PTC, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < crs_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_CRS, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < env_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_ENV, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < efo_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_EFO, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + + for (i = 0; i < lfo_cparam; i++) + ADSP_InterpParam( pnew, pmin, pmax, PRC_LFO, iskip, i, pa->size, ADSP_SIZE_MAX , 0); + +} + +// these convars store the index to the first preset for each shape type in dsp_presets.txt + +ConVar adsp_room_min ("adsp_room_min", "102"); +ConVar adsp_duct_min ("adsp_duct_min", "106"); +ConVar adsp_hall_min ("adsp_hall_min", "110"); +ConVar adsp_tunnel_min ("adsp_tunnel_min", "114"); +ConVar adsp_street_min ("adsp_street_min", "118"); +ConVar adsp_alley_min ("adsp_alley_min", "122"); +ConVar adsp_courtyard_min ("adsp_courtyard_min", "126"); +ConVar adsp_openspace_min ("adsp_openspace_min", "130"); +ConVar adsp_openwall_min ("adsp_openwall_min", "130"); +ConVar adsp_openstreet_min ("adsp_openstreet_min", "118"); +ConVar adsp_opencourtyard_min ("adsp_opencourtyard_min", "126"); + +// given room parameters, construct and return a dsp preset representing the room. +// bskyabove, width, length, height, fdiffusion, freflectivity are all passed-in room parameters +// psurf_refl is a passed-in array of reflectivity values for 6 surfaces +// inode is the location within g_psettemplates[] that the dsp preset will be constructed (inode = dsp preset#) +// cnode should always = DSP_CAUTO_PRESETS +// returns idsp preset. + +int DSP_ConstructPreset( bool bskyabove, int width, int length, int height, float fdiffusion, float freflectivity, float *psurf_refl, int inode, int cnodes ) +{ + auto_params_t ap; + auto_params_t *pa; + + pset_t new_pset; // preset + pset_t pset_min; + pset_t pset_max; + + int ipreset; + int ipset_min; + int ipset_max; + + if (inode >= DSP_CAUTO_PRESETS) + { + Assert(false); // check DAS_CNODES == DSP_CAUTO_PRESETS!!! + return 0; + } + + // fill parameter struct + + ap.bskyabove = bskyabove; + ap.width = width; + ap.length = length; + ap.height = height; + ap.fdiffusion = fdiffusion; + ap.freflectivity = freflectivity; + + for (int i = 0; i < 6; i++) + ap.surface_refl[i] = psurf_refl[i]; + + if (ap.bskyabove) + ap.surface_refl[4] = 0.0; + + // select shape, size based on params + + ADSP_GetAutoShape( &ap ); + + // set up min/max presets based on shape + + switch ( ap.shape ) + { + default: + case ADSP_ROOM: ipset_min = adsp_room_min.GetInt(); break; + case ADSP_DUCT: ipset_min = adsp_duct_min.GetInt(); break; + case ADSP_HALL: ipset_min = adsp_hall_min.GetInt(); break; + case ADSP_TUNNEL: ipset_min = adsp_tunnel_min.GetInt(); break; + case ADSP_STREET: ipset_min = adsp_street_min.GetInt(); break; + case ADSP_ALLEY: ipset_min = adsp_alley_min.GetInt(); break; + case ADSP_COURTYARD: ipset_min = adsp_courtyard_min.GetInt(); break; + case ADSP_OPEN_SPACE: ipset_min = adsp_openspace_min.GetInt(); break; + case ADSP_OPEN_WALL: ipset_min = adsp_openwall_min.GetInt(); break; + case ADSP_OPEN_STREET: ipset_min = adsp_openstreet_min.GetInt(); break; + case ADSP_OPEN_COURTYARD: ipset_min = adsp_opencourtyard_min.GetInt(); break; + } + + // presets in dsp_presets.txt are ordered as: + + // <shape><empty><min> + // <shape><empty><max> + // <shape><diffuse><min> + // <shape><diffuse><max> + pa = ≈ + if ( ADSP_IsDiffuse(pa) ) + ipset_min += 2; + + ipset_max = ipset_min + 1; + + pset_min = g_psettemplates[ipset_min]; + pset_max = g_psettemplates[ipset_max]; + + // given min and max preset and auto parameters, create new preset + + // interpolate between 1st instances of each processor type (ie: PRC_DLY) appearing in preset + + ADSP_InterpolatePreset( &new_pset, &pset_min, &pset_max, &ap, 0 ); + + // interpolate between 2nd instances of each processor type (ie: PRC_DLY) appearing in preset + + ADSP_InterpolatePreset( &new_pset, &pset_min, &pset_max, &ap, 1 ); + + // copy constructed preset back into node's template location + + ipreset = DSP_AUTO_BASE + inode; + + g_psettemplates[ipreset] = new_pset; + + return ipreset; +} + +/////////////////////////////////////// +// Helpers: called only from DSP_Process +/////////////////////////////////////// + + +// return true if batch processing version of preset exists + +inline bool FBatchPreset( pset_t *ppset ) +{ + + switch (ppset->type) + { + case PSET_LINEAR: + return true; + case PSET_SIMPLE: + return true; + default: + return false; + } +} + +// Helper: called only from DSP_Process +// mix front stereo buffer to mono buffer, apply dsp fx + +inline void DSP_ProcessStereoToMono(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + int count = sampleCount; + int av; + int x; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + if ( FBatchPreset(pdsp->ppset[0])) + { + // convert Stereo to Mono in place, then batch process fx: perf KDB + + // front->left + front->right / 2 into front->left, front->right duplicated. + + while ( count-- ) + { + pbf->left = (pbf->left + pbf->right) >> 1; + pbf++; + } + + // process left (mono), duplicate output into right + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT_DUPLICATE); + } + else + { + // avg left and right -> mono fx -> duplcate out left and right + while ( count-- ) + { + av = ( ( pbf->left + pbf->right ) >> 1 ); + x = PSET_GetNext( pdsp->ppset[0], av ); + x = CLIP_DSP( x ); + pbf->left = pbf->right = x; + pbf++; + } + } + return; + } + + // crossfading to current preset from previous preset + + if ( bcrossfading ) + { + int r; + int fl; + int fr; + int flp; + int frp; + int xf_fl; + int xf_fr; + bool bexp = pdsp->bexpfade; + bool bfadetostereo = (pdsp->ipset == 0); + bool bfadefromstereo = (pdsp->ipsetprev == 0); + + Assert ( !(bfadetostereo && bfadefromstereo) ); // don't call if ipset & ipsetprev both 0! + + if ( bfadetostereo || bfadefromstereo ) + { + // special case if fading to or from preset 0, stereo passthrough + + while ( count-- ) + { + av = ( ( pbf->left + pbf->right ) >> 1 ); + + // get current preset values + + if ( pdsp->ipset ) + { + fl = fr = PSET_GetNext( pdsp->ppset[0], av ); + } + else + { + fl = pbf->left; + fr = pbf->right; + } + + // get previous preset values + + if ( pdsp->ipsetprev ) + { + frp = flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + } + else + { + flp = pbf->left; + frp = pbf->right; + } + + fl = CLIP_DSP(fl); + fr = CLIP_DSP(fr); + flp = CLIP_DSP(flp); + frp = CLIP_DSP(frp); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); // crossfade front left previous to front left + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); // crossfade front left previous to front left + } + + pbf->left = xf_fl; // crossfaded front left, duplicate in right channel + pbf->right = xf_fr; + + pbf++; + } + + return; + } + + // crossfade mono to mono preset + + while ( count-- ) + { + av = ( ( pbf->left + pbf->right ) >> 1 ); + + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], av ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + + fl = CLIP_DSP(fl); + flp = CLIP_DSP(flp); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + + if (!bexp) + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + else + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + + pbf->left = xf_fl; // crossfaded front left, duplicate in right channel + pbf->right = xf_fl; + + pbf++; + } + } +} + +// Helper: called only from DSP_Process +// DSP_Process stereo in to stereo out (if more than 2 procs, ignore them) + +inline void DSP_ProcessStereoToStereo(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + int count = sampleCount; + int fl, fr; + + if ( !bcrossfading ) + { + + if ( !pdsp->ipset ) + return; + + if ( FBatchPreset(pdsp->ppset[0]) && FBatchPreset(pdsp->ppset[1]) ) + { + + // process left & right + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT ); + PSET_GetNextN( pdsp->ppset[1], pbfront, sampleCount, OP_RIGHT ); + } + else + { + // left -> left fx, right -> right fx + while ( count-- ) + { + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + + fl = CLIP_DSP( fl ); + fr = CLIP_DSP( fr ); + + pbf->left = fl; + pbf->right = fr; + pbf++; + } + } + return; + } + + // crossfading to current preset from previous preset + + if ( bcrossfading ) + { + int r; + int flp, frp; + int xf_fl, xf_fr; + bool bexp = pdsp->bexpfade; + + while ( count-- ) + { + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], pbf->left ); + frp = PSET_GetNext( pdsp->ppsetprev[1], pbf->right ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + fl = CLIP_DSP( fl ); + fr = CLIP_DSP( fr ); + flp = CLIP_DSP( flp ); + frp = CLIP_DSP( frp ); + + // crossfade from previous to current preset + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); + } + + pbf->left = xf_fl; // crossfaded front left + pbf->right = xf_fr; + + pbf++; + } + } +} + +// Helper: called only from DSP_Process +// DSP_Process quad in to mono out (front left = front right) + +inline void DSP_ProcessQuadToMono(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + portable_samplepair_t *pbr = pbrear; // pointer to buffer of rear stereo samples to process + int count = sampleCount; + int x; + int av; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + if ( FBatchPreset(pdsp->ppset[0]) ) + { + + // convert Quad to Mono in place, then batch process fx: perf KDB + + // left front + rear -> left, right front + rear -> right + while ( count-- ) + { + pbf->left = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + pbf++; + pbr++; + } + + // process left (mono), duplicate into right + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT_DUPLICATE); + + // copy processed front to rear + + count = sampleCount; + + pbf = pbfront; + pbr = pbrear; + + while ( count-- ) + { + pbr->left = pbf->left; + pbr->right = pbf->right; + pbf++; + pbr++; + } + + } + else + { + // avg fl,fr,rl,rr into mono fx, duplicate on all channels + while ( count-- ) + { + av = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + x = PSET_GetNext( pdsp->ppset[0], av ); + x = CLIP_DSP( x ); + pbr->left = pbr->right = pbf->left = pbf->right = x; + pbf++; + pbr++; + } + } + return; + } + + if ( bcrossfading ) + { + int r; + int fl, fr, rl, rr; + int flp, frp, rlp, rrp; + int xf_fl, xf_fr, xf_rl, xf_rr; + bool bexp = pdsp->bexpfade; + bool bfadetoquad = (pdsp->ipset == 0); + bool bfadefromquad = (pdsp->ipsetprev == 0); + + if ( bfadetoquad || bfadefromquad ) + { + // special case if previous or current preset is 0 (quad passthrough) + + while ( count-- ) + { + av = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + + // get current preset values + + // current preset is 0, which implies fading to passthrough quad output + // need to fade from mono to quad + + if ( pdsp->ipset ) + { + rl = rr = fl = fr = PSET_GetNext( pdsp->ppset[0], av ); + } + else + { + fl = pbf->left; + fr = pbf->right; + rl = pbr->left; + rr = pbr->right; + } + + // get previous preset values + + if ( pdsp->ipsetprev ) + { + rrp = rlp = frp = flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + } + else + { + flp = pbf->left; + frp = pbf->right; + rlp = pbr->left; + rrp = pbr->right; + } + + fl = CLIP_DSP(fl); + fr = CLIP_DSP(fr); + flp = CLIP_DSP(flp); + frp = CLIP_DSP(frp); + rl = CLIP_DSP(rl); + rr = CLIP_DSP(rr); + rlp = CLIP_DSP(rlp); + rrp = CLIP_DSP(rrp); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE(rr, rrp, r); // crossfade front left previous to front left + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE_EXP(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE_EXP(rr, rrp, r); // crossfade front left previous to front left + } + + pbf->left = xf_fl; + pbf->right = xf_fr; + pbr->left = xf_rl; + pbr->right = xf_rr; + + pbf++; + pbr++; + } + + return; + } + + while ( count-- ) + { + + av = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], av ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + fl = CLIP_DSP( fl ); + flp = CLIP_DSP( flp ); + + // crossfade from previous to current preset + if (!bexp) + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + else + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + + pbf->left = xf_fl; // crossfaded front left, duplicated to all channels + pbf->right = xf_fl; + pbr->left = xf_fl; + pbr->right = xf_fl; + + pbf++; + pbr++; + } + } +} + +// Helper: called only from DSP_Process +// DSP_Process quad in to stereo out (preserve stereo spatialization, throw away front/rear) + +inline void DSP_ProcessQuadToStereo(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + portable_samplepair_t *pbr = pbrear; // pointer to buffer of rear stereo samples to process + int count = sampleCount; + int fl, fr; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + if ( FBatchPreset(pdsp->ppset[0]) && FBatchPreset(pdsp->ppset[1]) ) + { + + // convert Quad to Stereo in place, then batch process fx: perf KDB + + // left front + rear -> left, right front + rear -> right + + while ( count-- ) + { + pbf->left = (pbf->left + pbr->left) >> 1; + pbf->right = (pbf->right + pbr->right) >> 1; + pbf++; + pbr++; + } + + // process left & right + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT); + PSET_GetNextN( pdsp->ppset[1], pbfront, sampleCount, OP_RIGHT ); + + // copy processed front to rear + + count = sampleCount; + + pbf = pbfront; + pbr = pbrear; + + while ( count-- ) + { + pbr->left = pbf->left; + pbr->right = pbf->right; + pbf++; + pbr++; + } + + } + else + { + // left front + rear -> left fx, right front + rear -> right fx + while ( count-- ) + { + fl = PSET_GetNext( pdsp->ppset[0], (pbf->left + pbr->left) >> 1); + fr = PSET_GetNext( pdsp->ppset[1], (pbf->right + pbr->right) >> 1); + fl = CLIP_DSP( fl ); + fr = CLIP_DSP( fr ); + + pbr->left = pbf->left = fl; + pbr->right = pbf->right = fr; + pbf++; + pbr++; + } + } + return; + } + + // crossfading to current preset from previous preset + + if ( bcrossfading ) + { + int r; + int rl, rr; + int flp, frp, rlp, rrp; + int xf_fl, xf_fr, xf_rl, xf_rr; + int avl, avr; + bool bexp = pdsp->bexpfade; + bool bfadetoquad = (pdsp->ipset == 0); + bool bfadefromquad = (pdsp->ipsetprev == 0); + + if ( bfadetoquad || bfadefromquad ) + { + // special case if previous or current preset is 0 (quad passthrough) + + while ( count-- ) + { + avl = (pbf->left + pbr->left) >> 1; + avr = (pbf->right + pbr->right) >> 1; + + // get current preset values + + // current preset is 0, which implies fading to passthrough quad output + // need to fade from stereo to quad + + if ( pdsp->ipset ) + { + rl = fl = PSET_GetNext( pdsp->ppset[0], avl ); + rr = fr = PSET_GetNext( pdsp->ppset[0], avr ); + } + else + { + fl = pbf->left; + fr = pbf->right; + rl = pbr->left; + rr = pbr->right; + } + + // get previous preset values + + if ( pdsp->ipsetprev ) + { + rlp = flp = PSET_GetNext( pdsp->ppsetprev[0], avl ); + rrp = frp = PSET_GetNext( pdsp->ppsetprev[0], avr ); + } + else + { + flp = pbf->left; + frp = pbf->right; + rlp = pbr->left; + rrp = pbr->right; + } + + fl = CLIP_DSP(fl); + fr = CLIP_DSP(fr); + flp = CLIP_DSP(flp); + frp = CLIP_DSP(frp); + rl = CLIP_DSP(rl); + rr = CLIP_DSP(rr); + rlp = CLIP_DSP(rlp); + rrp = CLIP_DSP(rrp); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE(rr, rrp, r); // crossfade front left previous to front left + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE_EXP(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE_EXP(rr, rrp, r); // crossfade front left previous to front left + } + + pbf->left = xf_fl; + pbf->right = xf_fr; + pbr->left = xf_rl; + pbr->right = xf_rr; + + pbf++; + pbr++; + } + + return; + } + + while ( count-- ) + { + avl = (pbf->left + pbr->left) >> 1; + avr = (pbf->right + pbr->right) >> 1; + + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], avl ); + fr = PSET_GetNext( pdsp->ppset[1], avr ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], avl ); + frp = PSET_GetNext( pdsp->ppsetprev[1], avr ); + + + fl = CLIP_DSP( fl ); + fr = CLIP_DSP( fr ); + + // get previous preset values + + flp = CLIP_DSP( flp ); + frp = CLIP_DSP( frp ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); + } + + pbf->left = xf_fl; // crossfaded front left + pbf->right = xf_fr; + + pbr->left = xf_fl; // duplicate front channel to rear channel + pbr->right = xf_fr; + + pbf++; + pbr++; + } + } +} + +// Helper: called only from DSP_Process +// DSP_Process quad in to quad out + +inline void DSP_ProcessQuadToQuad(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + portable_samplepair_t *pbr = pbrear; // pointer to buffer of rear stereo samples to process + int count = sampleCount; + int fl, fr, rl, rr; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + // each channel gets its own processor + + if ( FBatchPreset(pdsp->ppset[0]) && FBatchPreset(pdsp->ppset[1]) && FBatchPreset(pdsp->ppset[2]) && FBatchPreset(pdsp->ppset[3])) + { + // batch process fx front & rear, left & right: perf KDB + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT); + PSET_GetNextN( pdsp->ppset[1], pbfront, sampleCount, OP_RIGHT ); + PSET_GetNextN( pdsp->ppset[2], pbrear, sampleCount, OP_LEFT ); + PSET_GetNextN( pdsp->ppset[3], pbrear, sampleCount, OP_RIGHT ); + } + else + { + while ( count-- ) + { + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + rl = PSET_GetNext( pdsp->ppset[2], pbr->left ); + rr = PSET_GetNext( pdsp->ppset[3], pbr->right ); + + pbf->left = CLIP_DSP( fl ); + pbf->right = CLIP_DSP( fr ); + pbr->left = CLIP_DSP( rl ); + pbr->right = CLIP_DSP( rr ); + + pbf++; + pbr++; + } + } + return; + } + + // crossfading to current preset from previous preset + + if ( bcrossfading ) + { + int r; + int flp, frp, rlp, rrp; + int xf_fl, xf_fr, xf_rl, xf_rr; + bool bexp = pdsp->bexpfade; + + while ( count-- ) + { + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + rl = PSET_GetNext( pdsp->ppset[2], pbr->left ); + rr = PSET_GetNext( pdsp->ppset[3], pbr->right ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], pbf->left ); + frp = PSET_GetNext( pdsp->ppsetprev[1], pbf->right ); + rlp = PSET_GetNext( pdsp->ppsetprev[2], pbr->left ); + rrp = PSET_GetNext( pdsp->ppsetprev[3], pbr->right ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); + xf_rl = XFADE(rl, rlp, r); + xf_rr = XFADE(rr, rrp, r); + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); + xf_rl = XFADE_EXP(rl, rlp, r); + xf_rr = XFADE_EXP(rr, rrp, r); + } + + pbf->left = CLIP_DSP(xf_fl); // crossfaded front left + pbf->right = CLIP_DSP(xf_fr); + pbr->left = CLIP_DSP(xf_rl); + pbr->right = CLIP_DSP(xf_rr); + + pbf++; + pbr++; + } + } +} + + +// Helper: called only from DSP_Process +// DSP_Process quad + center in to mono out (front left = front right) + +inline void DSP_Process5To1(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, portable_samplepair_t *pbcenter, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + portable_samplepair_t *pbr = pbrear; // pointer to buffer of rear stereo samples to process + portable_samplepair_t *pbc = pbcenter; // pointer to buffer of center mono samples to process + int count = sampleCount; + int x; + int av; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + if ( FBatchPreset(pdsp->ppset[0]) ) + { + + // convert Quad + Center to Mono in place, then batch process fx: perf KDB + + // left front + rear -> left, right front + rear -> right + while ( count-- ) + { + // pbf->left = ((pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) / 5); + + av = (pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) * 51; // 51/255 = 1/5 + av >>= 8; + pbf->left = av; + pbf++; + pbr++; + pbc++; + } + + // process left (mono), duplicate into right + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT_DUPLICATE); + + // copy processed front to rear & center + + count = sampleCount; + + pbf = pbfront; + pbr = pbrear; + pbc = pbcenter; + + while ( count-- ) + { + pbr->left = pbf->left; + pbr->right = pbf->right; + pbc->left = pbf->left; + pbf++; + pbr++; + pbc++; + } + + } + else + { + // avg fl,fr,rl,rr,fc into mono fx, duplicate on all channels + while ( count-- ) + { + // av = ((pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) / 5); + av = (pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) * 51; // 51/255 = 1/5 + av >>= 8; + x = PSET_GetNext( pdsp->ppset[0], av ); + x = CLIP_DSP( x ); + pbr->left = pbr->right = pbf->left = pbf->right = pbc->left = x; + pbf++; + pbr++; + pbc++; + } + } + return; + } + + if ( bcrossfading ) + { + int r; + int fl, fr, rl, rr, fc; + int flp, frp, rlp, rrp, fcp; + int xf_fl, xf_fr, xf_rl, xf_rr, xf_fc; + bool bexp = pdsp->bexpfade; + bool bfadetoquad = (pdsp->ipset == 0); + bool bfadefromquad = (pdsp->ipsetprev == 0); + + if ( bfadetoquad || bfadefromquad ) + { + // special case if previous or current preset is 0 (quad passthrough) + + while ( count-- ) + { + // av = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + + av = (pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) * 51; // 51/255 = 1/5 + av >>= 8; + + // get current preset values + + // current preset is 0, which implies fading to passthrough quad output + // need to fade from mono to quad + + if ( pdsp->ipset ) + { + fc = rl = rr = fl = fr = PSET_GetNext( pdsp->ppset[0], av ); + } + else + { + fl = pbf->left; + fr = pbf->right; + rl = pbr->left; + rr = pbr->right; + fc = pbc->left; + } + + // get previous preset values + + if ( pdsp->ipsetprev ) + { + fcp = rrp = rlp = frp = flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + } + else + { + flp = pbf->left; + frp = pbf->right; + rlp = pbr->left; + rrp = pbr->right; + fcp = pbc->left; + } + + fl = CLIP_DSP(fl); + fr = CLIP_DSP(fr); + flp = CLIP_DSP(flp); + frp = CLIP_DSP(frp); + rl = CLIP_DSP(rl); + rr = CLIP_DSP(rr); + rlp = CLIP_DSP(rlp); + rrp = CLIP_DSP(rrp); + fc = CLIP_DSP(fc); + fcp = CLIP_DSP(fcp); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE(rr, rrp, r); // crossfade front left previous to front left + xf_fc = XFADE(fc, fcp, r); // crossfade front left previous to front left + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); // crossfade front left previous to front left + xf_rl = XFADE_EXP(rl, rlp, r); // crossfade front left previous to front left + xf_rr = XFADE_EXP(rr, rrp, r); // crossfade front left previous to front left + xf_fc = XFADE_EXP(fc, fcp, r); // crossfade front left previous to front left + } + + pbf->left = xf_fl; + pbf->right = xf_fr; + pbr->left = xf_rl; + pbr->right = xf_rr; + pbc->left = xf_fc; + + pbf++; + pbr++; + pbc++; + } + + return; + } + + while ( count-- ) + { + + // av = ((pbf->left + pbf->right + pbr->left + pbr->right) >> 2); + av = (pbf->left + pbf->right + pbr->left + pbr->right + pbc->left) * 51; // 51/255 = 1/5 + av >>= 8; + + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], av ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], av ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + fl = CLIP_DSP( fl ); + flp = CLIP_DSP( flp ); + + // crossfade from previous to current preset + if (!bexp) + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + else + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + + pbf->left = xf_fl; // crossfaded front left, duplicated to all channels + pbf->right = xf_fl; + pbr->left = xf_fl; + pbr->right = xf_fl; + pbc->left = xf_fl; + + pbf++; + pbr++; + pbc++; + } + } +} + +// Helper: called only from DSP_Process +// DSP_Process quad + center in to quad + center out + +inline void DSP_Process5To5(dsp_t *pdsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, portable_samplepair_t *pbcenter, int sampleCount, bool bcrossfading ) +{ + portable_samplepair_t *pbf = pbfront; // pointer to buffer of front stereo samples to process + portable_samplepair_t *pbr = pbrear; // pointer to buffer of rear stereo samples to process + portable_samplepair_t *pbc = pbcenter; // pointer to buffer of center mono samples to process + + int count = sampleCount; + int fl, fr, rl, rr, fc; + + if ( !bcrossfading ) + { + if ( !pdsp->ipset ) + return; + + // each channel gets its own processor + + if ( FBatchPreset(pdsp->ppset[0]) && FBatchPreset(pdsp->ppset[1]) && FBatchPreset(pdsp->ppset[2]) && FBatchPreset(pdsp->ppset[3])) + { + // batch process fx front & rear, left & right: perf KDB + + PSET_GetNextN( pdsp->ppset[0], pbfront, sampleCount, OP_LEFT); + PSET_GetNextN( pdsp->ppset[1], pbfront, sampleCount, OP_RIGHT ); + PSET_GetNextN( pdsp->ppset[2], pbrear, sampleCount, OP_LEFT ); + PSET_GetNextN( pdsp->ppset[3], pbrear, sampleCount, OP_RIGHT ); + PSET_GetNextN( pdsp->ppset[4], pbcenter, sampleCount, OP_LEFT ); + } + else + { + while ( count-- ) + { + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + rl = PSET_GetNext( pdsp->ppset[2], pbr->left ); + rr = PSET_GetNext( pdsp->ppset[3], pbr->right ); + fc = PSET_GetNext( pdsp->ppset[4], pbc->left ); + + pbf->left = CLIP_DSP( fl ); + pbf->right = CLIP_DSP( fr ); + pbr->left = CLIP_DSP( rl ); + pbr->right = CLIP_DSP( rr ); + pbc->left = CLIP_DSP( fc ); + + pbf++; + pbr++; + pbc++; + } + } + return; + } + + // crossfading to current preset from previous preset + + if ( bcrossfading ) + { + int r; + int flp, frp, rlp, rrp, fcp; + int xf_fl, xf_fr, xf_rl, xf_rr, xf_fc; + bool bexp = pdsp->bexpfade; + + while ( count-- ) + { + // get current preset values + + fl = PSET_GetNext( pdsp->ppset[0], pbf->left ); + fr = PSET_GetNext( pdsp->ppset[1], pbf->right ); + rl = PSET_GetNext( pdsp->ppset[2], pbr->left ); + rr = PSET_GetNext( pdsp->ppset[3], pbr->right ); + fc = PSET_GetNext( pdsp->ppset[4], pbc->left ); + + // get previous preset values + + flp = PSET_GetNext( pdsp->ppsetprev[0], pbf->left ); + frp = PSET_GetNext( pdsp->ppsetprev[1], pbf->right ); + rlp = PSET_GetNext( pdsp->ppsetprev[2], pbr->left ); + rrp = PSET_GetNext( pdsp->ppsetprev[3], pbr->right ); + fcp = PSET_GetNext( pdsp->ppsetprev[4], pbc->left ); + + // get current ramp value + + r = RMP_GetNext( &pdsp->xramp ); + + // crossfade from previous to current preset + if (!bexp) + { + xf_fl = XFADE(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE(fr, frp, r); + xf_rl = XFADE(rl, rlp, r); + xf_rr = XFADE(rr, rrp, r); + xf_fc = XFADE(fc, fcp, r); + } + else + { + xf_fl = XFADE_EXP(fl, flp, r); // crossfade front left previous to front left + xf_fr = XFADE_EXP(fr, frp, r); + xf_rl = XFADE_EXP(rl, rlp, r); + xf_rr = XFADE_EXP(rr, rrp, r); + xf_fc = XFADE_EXP(fc, fcp, r); + } + + pbf->left = CLIP_DSP(xf_fl); // crossfaded front left + pbf->right = CLIP_DSP(xf_fr); + pbr->left = CLIP_DSP(xf_rl); + pbr->right = CLIP_DSP(xf_rr); + pbc->left = CLIP_DSP(xf_fc); + + pbf++; + pbr++; + pbc++; + } + } +} + +// This is an evil hack, but we need to restore the old presets after letting the sound system update for a few frames, so we just +// "defer" the restore until the top of the next call to CheckNewDspPresets. I put in a bit of warning in case we ever have code +// outside of this time period modifying any of the dsp convars. It doesn't seem to be an issue just save/loading between levels +static bool g_bNeedPresetRestore = false; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +struct PreserveDSP_t +{ + ConVar *cvar; + float oldvalue; +}; + +static PreserveDSP_t g_PreserveDSP[] = +{ + { &dsp_room }, + { &dsp_water }, + { &dsp_player }, + { &dsp_facingaway }, + { &dsp_speaker }, + { &dsp_spatial }, + { &dsp_automatic } +}; + +//----------------------------------------------------------------------------- +// Purpose: Called at the top of CheckNewDspPresets to restore ConVars to real values +//----------------------------------------------------------------------------- +void DSP_CheckRestorePresets() +{ + if ( !g_bNeedPresetRestore ) + return; + + g_bNeedPresetRestore = false; + + int i; + int c = ARRAYSIZE( g_PreserveDSP ); + + // Restore + for ( i = 0 ; i < c; ++i ) + { + PreserveDSP_t& slot = g_PreserveDSP[ i ]; + + ConVar *cv = slot.cvar; + Assert( cv ); + if ( cv->GetFloat() != 0.0f ) + { + // NOTE: dsp_speaker is being (correctly) save/restored by maps, which would trigger this warning + //Warning( "DSP_CheckRestorePresets: Value of %s was changed between DSP_ClearState and CheckNewDspPresets, not restoring to old value\n", cv->GetName() ); + continue; + } + cv->SetValue( slot.oldvalue ); + } + + // reinit all dsp processors (only load preset file on engine init, however) + + AllocDsps( false ); + + // flush dsp automatic nodes + + g_bdas_init_nodes = 0; + g_bdas_room_init = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void DSP_ClearState() +{ + // if we already cleared dsp state, and a restore is pending, + // don't clear again + + if ( g_bNeedPresetRestore ) + return; + + // always save a cleared dsp automatic value to force reset of all adsp code + + dsp_automatic.SetValue(0); + + // Tracker 7155: YWB: This is a pretty ugly hack to zero out all of the dsp convars and bootstrap the dsp system into using them for a few frames + + int i; + int c = ARRAYSIZE( g_PreserveDSP ); + + for ( i = 0 ; i < c; ++i ) + { + PreserveDSP_t& slot = g_PreserveDSP[ i ]; + + ConVar *cv = slot.cvar; + Assert( cv ); + slot.oldvalue = cv->GetFloat(); + cv->SetValue( 0 ); + } + + // force all dsp presets to end crossfades, end one-shot presets, & release and reset all resources + // immediately. + + FreeDsps( false ); // free all dsp states, but don't discard preset templates + + // This forces the ConVars which we set to zero above to be reloaded to their old values at the time we issue the CheckNewDspPresets + // command. This seems to happen early enough in level changes were we don't appear to be trying to stomp real settings... + + g_bNeedPresetRestore = true; +} + +// return true if dsp's preset is one-shot and it has expired + +bool DSP_HasExpired( int idsp ) +{ + dsp_t *pdsp; + + Assert( idsp < CDSPS ); + + if (idsp < 0 || idsp >= CDSPS) + return false; + + pdsp = &dsps[idsp]; + + // if first preset has expired, dsp has expired + + if ( PSET_IsOneShot( pdsp->ppset[0] ) ) + return PSET_HasExpired( pdsp->ppset[0] ); + else + return false; +} + +// returns true if dsp is crossfading from previous dsp preset + +bool DSP_IsCrossfading( int idsp ) +{ + dsp_t *pdsp; + + Assert( idsp < CDSPS ); + + if (idsp < 0 || idsp >= CDSPS) + return false; + + pdsp = &dsps[idsp]; + + return !RMP_HitEnd( &pdsp->xramp ); + +} + +// returns previous preset # before oneshot preset was set + +int DSP_OneShotPrevious( int idsp ) +{ + dsp_t *pdsp; + int idsp_prev; + + Assert( idsp < CDSPS ); + + if (idsp < 0 || idsp >= CDSPS) + return 0; + + pdsp = &dsps[idsp]; + + idsp_prev = pdsp->ipsetsav_oneshot; + + return idsp_prev; +} + +// given idsp (processor index), return true if +// both current and previous presets are 0 for this processor + +bool DSP_PresetIsOff( int idsp ) +{ + dsp_t *pdsp; + + if (idsp < 0 || idsp >= CDSPS) + return true; + + Assert ( idsp < CDSPS ); // make sure idsp is valid + + pdsp = &dsps[idsp]; + + // if current and previous preset 0, return - preset 0 is 'off' + + return ( !pdsp->ipset && !pdsp->ipsetprev ); +} + +// returns true if dsp is off for room effects + +bool DSP_RoomDSPIsOff() +{ + return DSP_PresetIsOff( Get_idsp_room() ); +} + +// Main DSP processing routine: +// process samples in buffers using pdsp processor +// continue crossfade between 2 dsp processors if crossfading on switch +// pfront - front stereo buffer to process +// prear - rear stereo buffer to process (may be NULL) +// pcenter - front center mono buffer (may be NULL) +// sampleCount - number of samples in pbuf to process +// This routine also maps the # processing channels in the pdsp to the number of channels +// supplied. ie: if the pdsp has 4 channels and pbfront and pbrear are both non-null, the channels +// map 1:1 through the processors. + +void DSP_Process( int idsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, portable_samplepair_t *pbcenter, int sampleCount ) +{ + bool bcrossfading; + int cchan_in; // input channels (2,4 or 5) + int cprocs; // output cannels (1, 2 or 4) + dsp_t *pdsp; + + if (idsp < 0 || idsp >= CDSPS) + return; + + // Don't pull dsp data in if player is not connected (during load/level change) + if ( !g_pSoundServices->IsConnected() ) + return; + + Assert ( idsp < CDSPS ); // make sure idsp is valid + + pdsp = &dsps[idsp]; + + Assert (pbfront); + + // return right away if fx processing is turned off + + if ( dsp_off.GetInt() ) + return; + + // if current and previous preset 0, return - preset 0 is 'off' + + if ( !pdsp->ipset && !pdsp->ipsetprev ) + return; + + if ( sampleCount < 0 ) + return; + + bcrossfading = !RMP_HitEnd( &pdsp->xramp ); + + // if not crossfading, and previous channel is not null, free previous + + if ( !bcrossfading ) + DSP_FreePrevPreset( pdsp ); + + // if current and previous preset 0 (ie: just freed previous), return - preset 0 is 'off' + + if ( !pdsp->ipset && !pdsp->ipsetprev ) + return; + + cchan_in = (pbrear ? 4 : 2) + (pbcenter ? 1 : 0); + cprocs = pdsp->cchan; + + Assert(cchan_in == 2 || cchan_in == 4 || cchan_in == 5 ); + + // if oneshot preset, update the duration counter (only update front left counter) + + PSET_UpdateDuration( pdsp->ppset[0], sampleCount ); + + // NOTE: when mixing between different channel sizes, + // always AVERAGE down to fewer channels and DUPLICATE up more channels. + // The following routines always process cchan_in channels. + // ie: QuadToMono still updates 4 values in buffer + + // DSP_Process stereo in to mono out (ie: left and right are averaged) + + if ( cchan_in == 2 && cprocs == 1) + { + DSP_ProcessStereoToMono( pdsp, pbfront, pbrear, sampleCount, bcrossfading ); + return; + } + + // DSP_Process stereo in to stereo out (if more than 2 procs, ignore them) + + if ( cchan_in == 2 && cprocs >= 2) + { + DSP_ProcessStereoToStereo( pdsp, pbfront, pbrear, sampleCount, bcrossfading ); + return; + } + + + // DSP_Process quad in to mono out + + if ( cchan_in == 4 && cprocs == 1) + { + DSP_ProcessQuadToMono( pdsp, pbfront, pbrear, sampleCount, bcrossfading ); + return; + } + + + // DSP_Process quad in to stereo out (preserve stereo spatialization, loose front/rear) + + if ( cchan_in == 4 && cprocs == 2) + { + DSP_ProcessQuadToStereo( pdsp, pbfront, pbrear, sampleCount, bcrossfading ); + return; + } + + + // DSP_Process quad in to quad out + + if ( cchan_in == 4 && cprocs == 4) + { + DSP_ProcessQuadToQuad( pdsp, pbfront, pbrear, sampleCount, bcrossfading ); + return; + } + + // DSP_Process quad + center in to mono out + + if ( cchan_in == 5 && cprocs == 1) + { + DSP_Process5To1( pdsp, pbfront, pbrear, pbcenter, sampleCount, bcrossfading ); + return; + } + + if ( cchan_in == 5 && cprocs == 2) + { + // undone: not used in AllocDsps + Assert(false); + //DSP_Process5to2( pdsp, pbfront, pbrear, pbcenter, sampleCount, bcrossfading ); + return; + } + + if ( cchan_in == 5 && cprocs == 4) + { + // undone: not used in AllocDsps + Assert(false); + //DSP_Process5to4( pdsp, pbfront, pbrear, pbcenter, sampleCount, bcrossfading ); + return; + } + + // DSP_Process quad + center in to quad + center out + + if ( cchan_in == 5 && cprocs == 5) + { + DSP_Process5To5( pdsp, pbfront, pbrear, pbcenter, sampleCount, bcrossfading ); + return; + } + +} + +// DSP helpers + +// free all dsp processors + +void FreeDsps( bool bReleaseTemplateMemory ) +{ + + DSP_Free(idsp_room); + DSP_Free(idsp_water); + DSP_Free(idsp_player); + DSP_Free(idsp_facingaway); + DSP_Free(idsp_speaker); + DSP_Free(idsp_spatial); + DSP_Free(idsp_automatic); + + idsp_room = 0; + idsp_water = 0; + idsp_player = 0; + idsp_facingaway = 0; + idsp_speaker = 0; + idsp_spatial = 0; + idsp_automatic = 0; + + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + if ( pSpecialBuffer->nSpecialDSP != 0 ) + { + DSP_Free( pSpecialBuffer->idsp_specialdsp ); + pSpecialBuffer->idsp_specialdsp = 0; + pSpecialBuffer->nPrevSpecialDSP = 0; + pSpecialBuffer->nSpecialDSP = 0; + } + } + + DSP_FreeAll(); + + // only unlock and free psettemplate memory on engine shutdown + + if ( bReleaseTemplateMemory ) + DSP_ReleaseMemory(); +} + +// alloc dsp processors, load dsp preset array from file on engine init only + +bool AllocDsps( bool bLoadPresetFile ) +{ + int csurround = (g_AudioDevice->IsSurround() ? 2: 0); // surround channels to allocate + int ccenter = (g_AudioDevice->IsSurroundCenter() ? 1 : 0); // center channels to allocate + + DSP_InitAll( bLoadPresetFile ); + + idsp_room = -1; + idsp_water = -1; + idsp_player = -1; + idsp_facingaway = -1; + idsp_speaker = -1; + idsp_spatial = -1; + idsp_automatic = -1; + + // alloc dsp room channel (mono, stereo if dsp_stereo is 1) + + // dsp room is mono, 300ms default fade time + + idsp_room = DSP_Alloc( dsp_room.GetInt(), 200, 1 ); + + // dsp automatic overrides dsp_room, if dsp_room set to DSP_AUTOMATIC (1) + + idsp_automatic = DSP_Alloc( dsp_automatic.GetInt(), 200, 1 ) ; + + // alloc stereo or quad series processors for player or water + + // water and player presets are mono + + idsp_water = DSP_Alloc( dsp_water.GetInt(), 100, 1 ); + idsp_player = DSP_Alloc( dsp_player.GetInt(), 100, 1 ); + + // alloc facing away filters (stereo, quad or 5ch) + + idsp_facingaway = DSP_Alloc( dsp_facingaway.GetInt(), 100, 2 + csurround + ccenter ); + + // alloc speaker preset (mono) + + idsp_speaker = DSP_Alloc( dsp_speaker.GetInt(), 300, 1 ); + + // alloc spatial preset (2-5 chan) + + idsp_spatial = DSP_Alloc( dsp_spatial.GetInt(), 300, 2 + csurround + ccenter ); + + // init prev values + + ipset_room_prev = dsp_room.GetInt(); + ipset_water_prev = dsp_water.GetInt(); + ipset_player_prev = dsp_player.GetInt(); + ipset_facingaway_prev = dsp_facingaway.GetInt(); + ipset_room_typeprev = dsp_room_type.GetInt(); + ipset_speaker_prev = dsp_speaker.GetInt(); + ipset_spatial_prev = dsp_spatial.GetInt(); + ipset_automatic_prev = dsp_automatic.GetInt(); + + if (idsp_room < 0 || idsp_water < 0 || idsp_player < 0 || idsp_facingaway < 0 || idsp_speaker < 0 || idsp_spatial < 0 || idsp_automatic < 0) + { + DevMsg ("WARNING: DSP processor failed to initialize! \n" ); + + FreeDsps( true ); + return false; + } + + return true; +} + +// count number of dsp presets specified in preset file +// counts outer {} pairs, ignoring inner {} pairs. + +int DSP_CountFilePresets( const char *pstart ) +{ + int cpresets = 0; + bool binpreset = false; + bool blookleft = false; + + while ( 1 ) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; + + if ( com_token[0] == '{' ) // left paren + { + if (!binpreset) + { + cpresets++; // found preset: + blookleft = true; // look for another left + binpreset = true; + } + else + { + blookleft = false; // inside preset: next, look for matching right paren + } + + continue; + } + + if ( com_token[0] == '}' ) // right paren + { + if (binpreset) + { + if (!blookleft) // looking for right paren + { + blookleft = true; // found it, now look for another left + } + else + { + // expected inner left paren, found outer right - end of preset definition + binpreset = false; + blookleft = true; + } + } + else + { + // error - unexpected } paren + DevMsg("PARSE ERROR!!! dsp_presets.txt: unexpected '}' \n"); + continue; + } + } + + } + + return cpresets; +} + +struct dsp_stringmap_t +{ + char sz[33]; + int i; +}; + +// token map for dsp_preset.txt + +dsp_stringmap_t gdsp_stringmap[] = +{ + // PROCESSOR TYPE: + {"NULL", PRC_NULL}, + {"DLY", PRC_DLY}, + {"RVA", PRC_RVA}, + {"FLT", PRC_FLT}, + {"CRS", PRC_CRS}, + {"PTC", PRC_PTC}, + {"ENV", PRC_ENV}, + {"LFO", PRC_LFO}, + {"EFO", PRC_EFO}, + {"MDY", PRC_MDY}, + {"DFR", PRC_DFR}, + {"AMP", PRC_AMP}, + + // FILTER TYPE: + {"LP", FLT_LP}, + {"HP", FLT_HP}, + {"BP", FLT_BP}, + + // FILTER QUALITY: + {"LO", QUA_LO}, + {"MED", QUA_MED}, + {"HI", QUA_HI}, + {"VHI", QUA_VHI}, + + // DELAY TYPE: + {"PLAIN", DLY_PLAIN}, + {"ALLPASS", DLY_ALLPASS}, + {"LOWPASS", DLY_LOWPASS}, + {"DLINEAR", DLY_LINEAR}, + {"FLINEAR", DLY_FLINEAR}, + {"LOWPASS_4TAP",DLY_LOWPASS_4TAP}, + {"PLAIN_4TAP", DLY_PLAIN_4TAP}, + + // LFO TYPE: + {"SIN", LFO_SIN}, + {"TRI", LFO_TRI}, + {"SQR", LFO_SQR}, + {"SAW", LFO_SAW}, + {"RND", LFO_RND}, + {"LOG_IN", LFO_LOG_IN}, + {"LOG_OUT", LFO_LOG_OUT}, + {"LIN_IN", LFO_LIN_IN}, + {"LIN_OUT", LFO_LIN_OUT}, + + // ENVELOPE TYPE: + {"LIN", ENV_LIN}, + {"EXP", ENV_EXP}, + + // PRESET CONFIGURATION TYPE: + {"SIMPLE", PSET_SIMPLE}, + {"LINEAR", PSET_LINEAR}, + {"PARALLEL2", PSET_PARALLEL2}, + {"PARALLEL4", PSET_PARALLEL4}, + {"PARALLEL5", PSET_PARALLEL5}, + {"FEEDBACK", PSET_FEEDBACK}, + {"FEEDBACK3", PSET_FEEDBACK3}, + {"FEEDBACK4", PSET_FEEDBACK4}, + {"MOD1", PSET_MOD}, + {"MOD2", PSET_MOD2}, + {"MOD3", PSET_MOD3} +}; + +int gcdsp_stringmap = sizeof(gdsp_stringmap) / sizeof (dsp_stringmap_t); + +#define isnumber(c) (c == '+' || c == '-' || c == '0' || c == '1' || c == '2' || c == '3' || c == '4' || c == '5' || c == '6' || c == '7'|| c == '8' || c == '9')\ + +// given ptr to null term. string, return integer or float value from g_dsp_stringmap + +float DSP_LookupStringToken( char *psz, int ipset ) +{ + int i; + float fipset = (float)ipset; + + if (isnumber(psz[0])) + return atof(psz); + + for (i = 0; i < gcdsp_stringmap; i++) + { + if (!strcmpi(gdsp_stringmap[i].sz, psz)) + return gdsp_stringmap[i].i; + } + + // not found + + DevMsg("DSP PARSE ERROR! token not found in dsp_presets.txt. Preset: %3.0f \n", fipset ); + return 0; +} + +// load dsp preset file, parse presets into g_psettemplate array +// format for each preset: +// { <preset #> <preset type> <#processors> <gain> { <processor type> <param0>...<param15> } {...} {...} } + +#define CHAR_LEFT_PAREN '{' +#define CHAR_RIGHT_PAREN '}' + +// free preset template memory + +void DSP_ReleaseMemory( void ) +{ + if (g_psettemplates) + { + delete[] g_psettemplates; + g_psettemplates = NULL; + } +} + +bool DSP_LoadPresetFile( void ) +{ + char szFile[ MAX_OSPATH ]; + char *pbuffer; + const char *pstart; + bool bResult = false; + int cpresets; + int ipreset; + int itype; + int cproc; + float mix_min; + float mix_max; + float db_min; + float db_mixdrop; + int j; + bool fdone; + float duration; + float fadeout; + + Q_snprintf( szFile, sizeof( szFile ), "scripts/dsp_presets.txt" ); + + MEM_ALLOC_CREDIT(); + + CUtlBuffer buf; + + if ( !g_pFullFileSystem->ReadFile( szFile, "GAME", buf ) ) + { + Error( "DSP_LoadPresetFile: unable to open '%s'\n", szFile ); + return bResult; + } + pbuffer = (char *)buf.PeekGet(); // Use malloc - free at end of this routine + + pstart = pbuffer; + + // figure out how many presets we're loading - count outer parens. + + cpresets = DSP_CountFilePresets( pstart ); + + g_cpsettemplates = cpresets; + + g_psettemplates = new pset_t[cpresets]; + if (!g_psettemplates) + { + Warning( "DSP Preset Loader: Out of memory.\n"); + goto load_exit; + } + memset (g_psettemplates, 0, cpresets * sizeof(pset_t)); + + + // parse presets into g_psettemplates array + + pstart = pbuffer; + + // for each preset... + + for ( j = 0; j < cpresets; j++) + { + // check for end of file or next CHAR_LEFT_PAREN + + while (1) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; + + if ( com_token[0] != CHAR_LEFT_PAREN ) + continue; + + break; + } + + // found start of a new preset definition + + // get preset #, type, cprocessors, gain + + pstart = COM_Parse( pstart ); + ipreset = atoi( com_token ); + + pstart = COM_Parse( pstart ); + itype = (int)DSP_LookupStringToken( com_token , ipreset); + + pstart = COM_Parse( pstart ); + mix_min = atof( com_token ); + + pstart = COM_Parse( pstart ); + mix_max = atof( com_token ); + + pstart = COM_Parse( pstart ); + duration = atof( com_token ); + + pstart = COM_Parse( pstart ); + fadeout = atof( com_token ); + + pstart = COM_Parse( pstart ); + db_min = atof( com_token ); + + pstart = COM_Parse( pstart ); + db_mixdrop = atof( com_token ); + + + g_psettemplates[ipreset].fused = true; + g_psettemplates[ipreset].mix_min = mix_min; + g_psettemplates[ipreset].mix_max = mix_max; + g_psettemplates[ipreset].duration = duration; + g_psettemplates[ipreset].fade = fadeout; + g_psettemplates[ipreset].db_min = db_min; + g_psettemplates[ipreset].db_mixdrop = db_mixdrop; + + // parse each processor for this preset + + fdone = false; + cproc = 0; + + while (1) + { + // find CHAR_LEFT_PAREN - start of new processor + + while (1) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; + + if (com_token[0] == CHAR_LEFT_PAREN) + break; + + if (com_token[0] == CHAR_RIGHT_PAREN) + { + // if found right paren, no more processors: done with this preset + fdone = true; + break; + } + } + + if ( fdone ) + break; + + // get processor type + + pstart = COM_Parse( pstart ); + g_psettemplates[ipreset].prcs[cproc].type = (int)DSP_LookupStringToken( com_token, ipreset ); + + // get param 0..n or stop when hit closing CHAR_RIGHT_PAREN + + int ip = 0; + + while (1) + { + pstart = COM_Parse( pstart ); + + if ( strlen(com_token) <= 0) + break; + + if ( com_token[0] == CHAR_RIGHT_PAREN ) + break; + + g_psettemplates[ipreset].prcs[cproc].prm[ip++] = DSP_LookupStringToken( com_token, ipreset ); + + // cap at max params + + ip = min(ip, CPRCPARAMS); + } + + cproc++; + if (cproc > CPSET_PRCS) + DevMsg("DSP PARSE ERROR!!! dsp_presets.txt: missing } or too many processors in preset #: %d \n", ipreset); + cproc = min(cproc, CPSET_PRCS); // don't overflow # procs + } + + // if cproc == 1, type is always SIMPLE + + if ( cproc == 1) + itype = PSET_SIMPLE; + + g_psettemplates[ipreset].type = itype; + g_psettemplates[ipreset].cprcs = cproc; + + } + + bResult = true; + +load_exit: + return bResult; +} + +//----------------------------------------------------------------------------- +// Purpose: Called by client on level shutdown to clear ear ringing dsp effects +// could be extended to other stuff +//----------------------------------------------------------------------------- +void DSP_FastReset( int dspType ) +{ + int c = ARRAYSIZE( g_PreserveDSP ); + + // Restore + for ( int i = 0 ; i < c; ++i ) + { + PreserveDSP_t& slot = g_PreserveDSP[ i ]; + + if ( slot.cvar == &dsp_player ) + { + slot.oldvalue = dspType; + return; + } + } +} + +// Helper to check for change in preset of any of 4 processors +// if switching to a new preset, alloc new preset, simulate both presets in DSP_Process & xfade, +// called a few times per frame. + +void CheckNewDspPresets( void ) +{ + bool b_slow_cpu = dsp_slow_cpu.GetInt() == 0 ? false : true; + + DSP_CheckRestorePresets(); + + // room fx are on only if cpu is not slow + + int iroom = b_slow_cpu ? 0 : dsp_room.GetInt() ; + int ifacingaway = b_slow_cpu ? 0 : dsp_facingaway.GetInt(); + int iroomtype = b_slow_cpu ? 0 : dsp_room_type.GetInt(); + int ispatial = b_slow_cpu ? 0 : dsp_spatial.GetInt(); + int iautomatic = b_slow_cpu ? 0 : dsp_automatic.GetInt(); + + // always use dsp to process these + + int iwater = dsp_water.GetInt(); + int iplayer = dsp_player.GetInt(); + int ispeaker = dsp_speaker.GetInt(); + + // check for expired one-shot presets on player and room. + // Only check if a) no new preset has been set and b) not crossfading from previous preset (ie; previous is null) + + if ( iplayer == ipset_player_prev && !DSP_IsCrossfading( idsp_player ) ) + { + if ( DSP_HasExpired ( idsp_player ) ) + { + iplayer = DSP_OneShotPrevious( idsp_player); // preset has expired - revert to previous preset before one-shot + dsp_player.SetValue(iplayer); + } + } + + if ( iroom == ipset_room_prev && !DSP_IsCrossfading( idsp_room ) ) + { + if ( DSP_HasExpired ( idsp_room ) ) + { + iroom = DSP_OneShotPrevious( idsp_room ); // preset has expired - revert to previous preset before one-shot + dsp_room.SetValue(iroom); + } + } + + + // legacy code support for "room_type" Cvar + + if ( iroomtype != ipset_room_typeprev ) + { + // force dsp_room = room_type + + ipset_room_typeprev = iroomtype; + dsp_room.SetValue(iroomtype); + } + + // NOTE: don't change presets if currently crossfading from a previous preset + + if ( iroom != ipset_room_prev && !DSP_IsCrossfading( idsp_room) ) + { + DSP_SetPreset( idsp_room, iroom ); + ipset_room_prev = iroom; + + // force room_type = dsp_room + + dsp_room_type.SetValue(iroom); + ipset_room_typeprev = iroom; + } + + if ( iwater != ipset_water_prev && !DSP_IsCrossfading( idsp_water) ) + { + DSP_SetPreset( idsp_water, iwater ); + ipset_water_prev = iwater; + } + + if ( iplayer != ipset_player_prev && !DSP_IsCrossfading( idsp_player)) + { + DSP_SetPreset( idsp_player, iplayer ); + ipset_player_prev = iplayer; + } + + if ( ifacingaway != ipset_facingaway_prev && !DSP_IsCrossfading( idsp_facingaway) ) + { + DSP_SetPreset( idsp_facingaway, ifacingaway ); + ipset_facingaway_prev = ifacingaway; + } + + if ( ispeaker != ipset_speaker_prev && !DSP_IsCrossfading( idsp_speaker) ) + { + DSP_SetPreset( idsp_speaker, ispeaker ); + ipset_speaker_prev = ispeaker; + } + + if ( ispatial != ipset_spatial_prev && !DSP_IsCrossfading( idsp_spatial) ) + { + DSP_SetPreset( idsp_spatial, ispatial ); + ipset_spatial_prev = ispatial; + } + + if ( iautomatic != ipset_automatic_prev && !DSP_IsCrossfading( idsp_automatic) ) + { + DSP_SetPreset( idsp_automatic, iautomatic ); + ipset_automatic_prev = iautomatic; + } + + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + if ( pSpecialBuffer->nSpecialDSP != pSpecialBuffer->nPrevSpecialDSP && !DSP_IsCrossfading( pSpecialBuffer->idsp_specialdsp ) ) + { + DSP_SetPreset( pSpecialBuffer->idsp_specialdsp, pSpecialBuffer->nSpecialDSP ); + pSpecialBuffer->nPrevSpecialDSP = pSpecialBuffer->nSpecialDSP; + } + } +} + +// create idsp_room preset from set of values, reload the preset. +// modifies psettemplates in place. + +// ipreset is the preset # ie: 40 +// iproc is the processor to modify within the preset (typically 0) +// pvalues is an array of floating point parameters +// cparams is the # of elements in pvalues + +// USED FOR DEBUG ONLY. + +void DSP_DEBUGSetParams(int ipreset, int iproc, float *pvalues, int cparams) +{ + pset_t new_pset; // preset + int cparam = clamp (cparams, 0, CPRCPARAMS); + prc_t *pprct; + + // copy template preset from template array + + new_pset = g_psettemplates[ipreset]; + + // get iproc processor + + pprct = &(new_pset.prcs[iproc]); + + // copy parameters in to processor + + for (int i = 0; i < cparam; i++) + { + pprct->prm[i] = pvalues[i]; + } + + // copy constructed preset back into template location + + g_psettemplates[ipreset] = new_pset; + + // setup new preset + + dsp_room.SetValue( 0 ); + + CheckNewDspPresets(); + + dsp_room.SetValue( ipreset ); + + CheckNewDspPresets(); +} + +// reload entire preset file, reset all current dsp presets +// NOTE: this is debug code only. It doesn't do all mem free work correctly! + +void DSP_DEBUGReloadPresetFile( void ) +{ + int iroom = dsp_room.GetInt(); + int iwater = dsp_water.GetInt(); + int iplayer = dsp_player.GetInt(); +// int ifacingaway = dsp_facingaway.GetInt(); +// int iroomtype = dsp_room_type.GetInt(); + int ispeaker = dsp_speaker.GetInt(); + int ispatial = dsp_spatial.GetInt(); +// int iautomatic = dsp_automatic.GetInt(); + + // reload template array + + DSP_ReleaseMemory(); + + DSP_LoadPresetFile(); + + // force presets to reload + + dsp_room.SetValue( 0 ); + dsp_water.SetValue( 0 ); + dsp_player.SetValue( 0 ); + //dsp_facingaway.SetValue( 0 ); + //dsp_room_type.SetValue( 0 ); + dsp_speaker.SetValue( 0 ); + dsp_spatial.SetValue( 0 ); + //dsp_automatic.SetValue( 0 ); + + CUtlVector< int > specialDSPs; + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + + specialDSPs.AddToTail( pSpecialBuffer->nSpecialDSP ); + pSpecialBuffer->nSpecialDSP = 0; + } + + CheckNewDspPresets(); + + dsp_room.SetValue( iroom ); + dsp_water.SetValue( iwater ); + dsp_player.SetValue( iplayer ); + //dsp_facingaway.SetValue( ifacingaway ); + //dsp_room_type.SetValue( iroomtype ); + dsp_speaker.SetValue( ispeaker ); + dsp_spatial.SetValue( ispatial ); + //dsp_automatic.SetValue( iautomatic ); + + int nSpecialDSPNum = 0; + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + + pSpecialBuffer->nSpecialDSP = specialDSPs[ nSpecialDSPNum ]; + nSpecialDSPNum++; + } + + CheckNewDspPresets(); + + // flush dsp automatic nodes + + g_bdas_init_nodes = 0; + g_bdas_room_init = 0; +} + +// UNDONE: stock reverb presets: + +// carpet hallway +// tile hallway +// wood hallway +// metal hallway + +// train tunnel +// sewer main tunnel +// concrete access tunnel +// cave tunnel +// sand floor cave tunnel + +// metal duct shaft +// elevator shaft +// large elevator shaft + +// parking garage +// aircraft hangar +// cathedral +// train station + +// small cavern +// large cavern +// huge cavern +// watery cavern +// long, low cavern + +// wood warehouse +// metal warehouse +// concrete warehouse + +// small closet room +// medium drywall room +// medium wood room +// medium metal room + +// elevator +// small metal room +// medium metal room +// large metal room +// huge metal room + +// small metal room dense +// medium metal room dense +// large metal room dense +// huge metal room dense + +// small concrete room +// medium concrete room +// large concrete room +// huge concrete room + +// small concrete room dense +// medium concrete room dense +// large concrete room dense +// huge concrete room dense + +// soundproof room +// carpet lobby +// swimming pool +// open park +// open courtyard +// wide parkinglot +// narrow street +// wide street, short buildings +// wide street, tall buildings +// narrow canyon +// wide canyon +// huge canyon +// small valley +// wide valley +// wreckage & rubble +// small building cluster +// wide open plain +// high vista + +// alien interior small +// alien interior medium +// alien interior large +// alien interior huge + +// special fx presets: + +// alien citadel + +// teleport aftershock (these presets all ADSR timeout and reset the dsp_* to 0) +// on target teleport +// off target teleport +// death fade +// beam stasis +// scatterbrain +// pulse only +// slomo +// hypersensitive +// supershocker +// physwhacked +// forcefieldfry +// juiced +// zoomed in +// crabbed +// barnacle gut +// bad transmission + +//////////////////////// +// Dynamics processing +//////////////////////// + +// compressor defines +#define COMP_MAX_AMP 32767 // abs max amplitude +#define COMP_THRESH 20000 // start compressing at this threshold + +// compress input value - smoothly limit output y to -32767 <= y <= 32767 +// UNDONE: not tested or used + +inline int S_Compress( int xin ) +{ + + return CLIP( xin >> 2 ); // DEBUG - disabled + + + float Yn, Xn, Cn, Fn; + float C0 = 20000; // threshold + float p = .3; // compression ratio + float g = 1; // gain after compression + + Xn = (float)xin; + + // Compressor formula: + // Cn = l*Cn-1 + (1-l)*|Xn| // peak detector with memory + // f(Cn) = (Cn/C0)^(p-1) for Cn > C0 // gain function above threshold + // f(Cn) = 1 for C <= C0 // unity gain below threshold + // Yn = f(Cn) * Xn // compressor output + + // UNDONE: curves discontinuous at threshold, causes distortion, try catmul-rom + + //float l = .5; // compressor memory + //Cn = l * (*pCnPrev) + (1 - l) * fabs((float)xin); + //*pCnPrev = Cn; + + Cn = fabs((float)xin); + + if (Cn < C0) + Fn = 1; + else + Fn = powf((Cn / C0),(p - 1)); + + Yn = Fn * Xn * g; + + //if (Cn > 0) + // Msg("%d -> %d\n", xin, (int)Yn); // DEBUG + + //if (fabs(Yn) > 32767) + // Yn = Yn; // DEBUG + + return (CLIP((int)Yn)); +} + diff --git a/engine/audio/private/snd_env_fx.h b/engine/audio/private/snd_env_fx.h new file mode 100644 index 0000000..cad7d72 --- /dev/null +++ b/engine/audio/private/snd_env_fx.h @@ -0,0 +1,61 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_ENV_FX_H +#define SND_ENV_FX_H + +#if defined( _WIN32 ) +#pragma once +#endif + +//===================================================================== +// FX presets +//===================================================================== + +#define SXROOM_OFF 0 + +#define SXROOM_GENERIC 1 // general, low reflective, diffuse room + +#define SXROOM_METALIC_S 2 // highly reflective, parallel surfaces +#define SXROOM_METALIC_M 3 +#define SXROOM_METALIC_L 4 + +#define SXROOM_TUNNEL_S 5 // resonant reflective, long surfaces +#define SXROOM_TUNNEL_M 6 +#define SXROOM_TUNNEL_L 7 + +#define SXROOM_CHAMBER_S 8 // diffuse, moderately reflective surfaces +#define SXROOM_CHAMBER_M 9 +#define SXROOM_CHAMBER_L 10 + +#define SXROOM_BRITE_S 11 // diffuse, highly reflective +#define SXROOM_BRITE_M 12 +#define SXROOM_BRITE_L 13 + +#define SXROOM_WATER1 14 // underwater fx +#define SXROOM_WATER2 15 +#define SXROOM_WATER3 16 + +#define SXROOM_CONCRETE_S 17 // bare, reflective, parallel surfaces +#define SXROOM_CONCRETE_M 18 +#define SXROOM_CONCRETE_L 19 + +#define SXROOM_OUTSIDE1 20 // echoing, moderately reflective +#define SXROOM_OUTSIDE2 21 // echoing, dull +#define SXROOM_OUTSIDE3 22 // echoing, very dull + +#define SXROOM_CAVERN_S 23 // large, echoing area +#define SXROOM_CAVERN_M 24 +#define SXROOM_CAVERN_L 25 + +#define SXROOM_WEIRDO1 26 +#define SXROOM_WEIRDO2 27 +#define SXROOM_WEIRDO3 28 + +#define CSXROOM 29 + +#endif // SND_ENV_FX_H diff --git a/engine/audio/private/snd_fixedint.h b/engine/audio/private/snd_fixedint.h new file mode 100644 index 0000000..b1e1aac --- /dev/null +++ b/engine/audio/private/snd_fixedint.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_FIXEDINT_H +#define SND_FIXEDINT_H + +#if defined( _WIN32 ) +#pragma once +#endif + +// fixed point stuff for real-time resampling +#define FIX_BITS 28 +#define FIX_SCALE (1 << FIX_BITS) +#define FIX_MASK ((1 << FIX_BITS)-1) +#define FIX_FLOAT(a) ((int)((a) * FIX_SCALE)) +#define FIX(a) (((int)(a)) << FIX_BITS) +#define FIX_INTPART(a) (((int)(a)) >> FIX_BITS) +#define FIX_FRACTION(a,b) (FIX(a)/(b)) +#define FIX_FRACPART(a) ((a) & FIX_MASK) +#define FIX_TODOUBLE(a) ((double)(a) / (double)FIX_SCALE) + +typedef unsigned int fixedint; + +#define FIX_BITS14 14 +#define FIX_SCALE14 (1 << FIX_BITS14) +#define FIX_MASK14 ((1 << FIX_BITS14)-1) +#define FIX_FLOAT14(a) ((int)((a) * FIX_SCALE14)) +#define FIX14(a) (((int)(a)) << FIX_BITS14) +#define FIX_INTPART14(a) (((int)(a)) >> FIX_BITS14) +#define FIX_FRACTION14(a,b) (FIX14(a)/(b)) +#define FIX_FRACPART14(a) ((a) & FIX_MASK14) +#define FIX_14TODOUBLE(a) ((double)(a) / (double)FIX_SCALE14) + +#define FIX_28TO14(a) ( (int)( ((unsigned int)(a)) >> (FIX_BITS - 14) ) ) + +#endif // SND_FIXEDINT_H diff --git a/engine/audio/private/snd_mix.cpp b/engine/audio/private/snd_mix.cpp new file mode 100644 index 0000000..ca44cbf --- /dev/null +++ b/engine/audio/private/snd_mix.cpp @@ -0,0 +1,4293 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Portable code to mix sounds for snd_dma.cpp. +// +//=============================================================================// + +#include "audio_pch.h" + +#include "mouthinfo.h" +#include "../../cl_main.h" +#include "icliententitylist.h" +#include "icliententity.h" +#include "../../sys_dll.h" +#include "video/ivideoservices.h" +#include "engine/IEngineSound.h" + +#if defined( REPLAY_ENABLED ) +#include "demo.h" +#include "replay_internal.h" +#endif +#ifdef GNUC +// we don't suport the ASM in this file right now under GCC, fallback to C libs +#undef id386 +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#if defined( REPLAY_ENABLED ) +extern IReplayMovieManager *g_pReplayMovieManager; +#endif + +#if defined(_WIN32) && id386 +// warning C4731: frame pointer register 'ebp' modified by inline assembly code +#pragma warning(disable : 4731) +#endif + +// NOTE: !!!!!! YOU MUST UPDATE SND_MIXA.S IF THIS VALUE IS CHANGED !!!!! +#define SND_SCALE_BITS 7 +#define SND_SCALE_SHIFT (8-SND_SCALE_BITS) +#define SND_SCALE_LEVELS (1<<SND_SCALE_BITS) + +#define SND_SCALE_BITS16 8 +#define SND_SCALE_SHIFT16 (8-SND_SCALE_BITS16) +#define SND_SCALE_LEVELS16 (1<<SND_SCALE_BITS16) + +void Snd_WriteLinearBlastStereo16(void); +void SND_PaintChannelFrom8( portable_samplepair_t *pOutput, int *volume, byte *pData8, int count ); +bool Con_IsVisible( void ); +void SND_RecordBuffer( void ); +bool DSP_RoomDSPIsOff( void ); +bool BChannelLowVolume( channel_t *pch, int vol_min ); +void ChannelCopyVolumes( channel_t *pch, int *pvolume_dest, int ivol_start, int cvol ); +float ChannelLoudestCurVolume( const channel_t * RESTRICT pch ); + +extern int g_soundtime; +extern float host_frametime; +extern float host_frametime_unbounded; + + +#if !defined( NO_VOICE ) +extern int g_SND_VoiceOverdriveInt; +#endif + +extern ConVar dsp_room; +extern ConVar dsp_water; +extern ConVar dsp_player; +extern ConVar dsp_facingaway; +extern ConVar snd_showstart; +extern ConVar dsp_automatic; +extern ConVar snd_pitchquality; + +extern float DSP_ROOM_MIX; +extern float DSP_NOROOM_MIX; + +portable_samplepair_t *g_paintbuffer; + +// temp paintbuffer - not included in main list of paintbuffers +// NOTE: this paintbuffer is also used as a copy buffer by interpolating pitch +// shift routines. Decreasing TEMP_COPY_BUFFER_SIZE (or PAINTBUFFER_MEM_SIZE) +// will decrease the maximum pitch level (current 4.0)! +portable_samplepair_t *g_temppaintbuffer = NULL; + +CUtlVector< paintbuffer_t > g_paintBuffers; + + +// pointer to current paintbuffer (front and reare), used by all mixing, upsampling and dsp routines +portable_samplepair_t *g_curpaintbuffer = NULL; +portable_samplepair_t *g_currearpaintbuffer = NULL; +portable_samplepair_t *g_curcenterpaintbuffer = NULL; + +bool g_bdirectionalfx; +bool g_bDspOff; +float g_dsp_volume; + +// dsp performance timing +unsigned g_snd_call_time_debug = 0; +unsigned g_snd_time_debug = 0; +unsigned g_snd_count_debug = 0; +unsigned g_snd_samplecount = 0; +unsigned g_snd_frametime = 0; +unsigned g_snd_frametime_total = 0; +int g_snd_profile_type = 0; // type 1 dsp, type 2 mixer, type 3 load sound, type 4 all sound + +#define FILTERTYPE_NONE 0 +#define FILTERTYPE_LINEAR 1 +#define FILTERTYPE_CUBIC 2 + +// filter memory for upsampling +portable_samplepair_t cubicfilter1[3] = {{0,0},{0,0},{0,0}}; +portable_samplepair_t cubicfilter2[3] = {{0,0},{0,0},{0,0}}; + +portable_samplepair_t linearfilter1[1] = {{0,0}}; +portable_samplepair_t linearfilter2[1] = {{0,0}}; +portable_samplepair_t linearfilter3[1] = {{0,0}}; +portable_samplepair_t linearfilter4[1] = {{0,0}}; +portable_samplepair_t linearfilter5[1] = {{0,0}}; +portable_samplepair_t linearfilter6[1] = {{0,0}}; +portable_samplepair_t linearfilter7[1] = {{0,0}}; +portable_samplepair_t linearfilter8[1] = {{0,0}}; + +int snd_scaletable[SND_SCALE_LEVELS][256]; // 32k*4 = 128K + +int *snd_p, snd_linear_count, snd_vol; +short *snd_out; + +extern int DSP_Alloc( int ipset, float xfade, int cchan ); + +bool DSP_CheckDspAutoEnabled( void ); +int Get_idsp_room ( void ); +int dsp_room_GetInt ( void ); +void DSP_SetDspAuto( int dsp_preset ); +bool DSP_CheckDspAutoEnabled( void ); + +void MIX_ScalePaintBuffer( int bufferIndex, int count, float fgain ); + +bool IsReplayRendering() +{ +#if defined( REPLAY_ENABLED ) + return g_pReplayMovieManager && g_pReplayMovieManager->IsRendering(); +#else + return false; +#endif +} + +//----------------------------------------------------------------------------- +// Free allocated memory buffers +//----------------------------------------------------------------------------- +void MIX_FreeAllPaintbuffers(void) +{ + if ( g_paintBuffers.Count() ) + { + if ( g_temppaintbuffer ) + { + _aligned_free( g_temppaintbuffer ); + g_temppaintbuffer = NULL; + } + + for ( int i = 0; i < g_paintBuffers.Count(); i++ ) + { + if ( g_paintBuffers[i].pbuf ) + { + _aligned_free( g_paintBuffers[i].pbuf ); + } + if ( g_paintBuffers[i].pbufrear ) + { + _aligned_free( g_paintBuffers[i].pbufrear ); + } + if ( g_paintBuffers[i].pbufcenter ) + { + _aligned_free( g_paintBuffers[i].pbufcenter ); + } + } + + g_paintBuffers.RemoveAll(); + } +} + +void MIX_InitializePaintbuffer( paintbuffer_t *pPaintBuffer, bool bSurround, bool bSurroundCenter ) +{ + V_memset( pPaintBuffer, 0, sizeof( *pPaintBuffer ) ); + + pPaintBuffer->pbuf = (portable_samplepair_t *)_aligned_malloc( PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t), 16 ); + V_memset( pPaintBuffer->pbuf, 0, PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t) ); + + if ( bSurround ) + { + pPaintBuffer->pbufrear = (portable_samplepair_t *)_aligned_malloc( PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t), 16 ); + V_memset( pPaintBuffer->pbufrear, 0, PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t) ); + } + if ( bSurroundCenter ) + { + pPaintBuffer->pbufcenter = (portable_samplepair_t *)_aligned_malloc( PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t), 16 ); + V_memset( pPaintBuffer->pbufcenter, 0, PAINTBUFFER_MEM_SIZE*sizeof(portable_samplepair_t) ); + } +} + +//----------------------------------------------------------------------------- +// Allocate memory buffers +// Initialize paintbuffers array, set current paint buffer to main output buffer SOUND_BUFFER_PAINT +//----------------------------------------------------------------------------- +bool MIX_InitAllPaintbuffers(void) +{ + bool bSurround; + bool bSurroundCenter; + + bSurroundCenter = g_AudioDevice->IsSurroundCenter(); + bSurround = g_AudioDevice->IsSurround() || bSurroundCenter; + + g_temppaintbuffer = (portable_samplepair_t*)_aligned_malloc( TEMP_COPY_BUFFER_SIZE*sizeof(portable_samplepair_t), 16 ); + V_memset( g_temppaintbuffer, 0, TEMP_COPY_BUFFER_SIZE*sizeof(portable_samplepair_t) ); + + while ( g_paintBuffers.Count() < SOUND_BUFFER_BASETOTAL ) + { + int nIndex = g_paintBuffers.AddToTail(); + MIX_InitializePaintbuffer( &(g_paintBuffers[ nIndex ]), bSurround, bSurroundCenter ); + } + + g_paintbuffer = g_paintBuffers[SOUND_BUFFER_PAINT].pbuf; + + // buffer flags + g_paintBuffers[SOUND_BUFFER_ROOM].flags = SOUND_BUSS_ROOM; + g_paintBuffers[SOUND_BUFFER_FACING].flags = SOUND_BUSS_FACING; + g_paintBuffers[SOUND_BUFFER_FACINGAWAY].flags = SOUND_BUSS_FACINGAWAY; + g_paintBuffers[SOUND_BUFFER_SPEAKER].flags = SOUND_BUSS_SPEAKER; + g_paintBuffers[SOUND_BUFFER_DRY].flags = SOUND_BUSS_DRY; + + // buffer surround sound flag + g_paintBuffers[SOUND_BUFFER_PAINT].fsurround = bSurround; + g_paintBuffers[SOUND_BUFFER_FACING].fsurround = bSurround; + g_paintBuffers[SOUND_BUFFER_FACINGAWAY].fsurround = bSurround; + g_paintBuffers[SOUND_BUFFER_DRY].fsurround = bSurround; + + // buffer 5 channel surround sound flag + g_paintBuffers[SOUND_BUFFER_PAINT].fsurround_center = bSurroundCenter; + g_paintBuffers[SOUND_BUFFER_FACING].fsurround_center = bSurroundCenter; + g_paintBuffers[SOUND_BUFFER_FACINGAWAY].fsurround_center = bSurroundCenter; + g_paintBuffers[SOUND_BUFFER_DRY].fsurround_center = bSurroundCenter; + + // room buffer mixes down to mono or stereo, never to 4 or 5 ch + g_paintBuffers[SOUND_BUFFER_ROOM].fsurround = false; + g_paintBuffers[SOUND_BUFFER_ROOM].fsurround_center = false; + + // speaker buffer mixes to mono + g_paintBuffers[SOUND_BUFFER_SPEAKER].fsurround = false; + g_paintBuffers[SOUND_BUFFER_SPEAKER].fsurround_center = false; + + MIX_SetCurrentPaintbuffer( SOUND_BUFFER_PAINT ); + + return true; +} + +// called before loading samples to mix - cap the mix rate (ie: pitch) so that +// we never overflow the mix copy buffer. + +double MIX_GetMaxRate( double rate, int sampleCount ) +{ + if (rate <= 2.0) + return rate; + + // copybuf_bytes = rate_max * samples_max * samplesize_max + // so: + // rate_max = copybuf_bytes / (samples_max * samplesize_max ) + + double samplesize_max = 4.0; // stereo 16bit samples + double copybuf_bytes = (double)(TEMP_COPY_BUFFER_SIZE * sizeof(portable_samplepair_t)); + double samples_max = (double)(PAINTBUFFER_SIZE); + + double rate_max = copybuf_bytes / (samples_max * samplesize_max); + + // make sure sampleCount is never greater than paintbuffer samples + // (this should have been set up in MIX_PaintChannels) + + Assert (sampleCount <= PAINTBUFFER_SIZE); + + return fpmin( rate, rate_max ); +} + + +// Transfer (endtime - lpaintedtime) stereo samples in pfront out to hardware +// pfront - pointer to stereo paintbuffer - 32 bit samples, interleaved stereo +// lpaintedtime - total number of 32 bit stereo samples previously output to hardware +// endtime - total number of 32 bit stereo samples currently mixed in paintbuffer +void S_TransferStereo16( void *pOutput, const portable_samplepair_t *pfront, int lpaintedtime, int endtime ) +{ + int lpos; + + if ( IsX360() ) + { + // not the right path for 360 + Assert( 0 ); + return; + } + + Assert( pOutput ); + + snd_vol = S_GetMasterVolume()*256; + snd_p = (int *)pfront; + + // get size of output buffer in full samples (LR pairs) + int samplePairCount = g_AudioDevice->DeviceSampleCount() >> 1; + int sampleMask = samplePairCount - 1; + + bool bShouldPlaySound = !cl_movieinfo.IsRecording() && !IsReplayRendering(); + + while ( lpaintedtime < endtime ) + { + // pbuf can hold 16384, 16 bit L/R samplepairs. + // lpaintedtime - where to start painting into dma buffer. + // (modulo size of dma buffer for current position). + // handle recirculating buffer issues + // lpos - samplepair index into dma buffer. First samplepair from paintbuffer to be xfered here. + lpos = lpaintedtime & sampleMask; + + // snd_out is L/R sample index into dma buffer. First L sample from paintbuffer goes here. + snd_out = (short *)pOutput + (lpos<<1); + + // snd_linear_count is number of samplepairs between end of dma buffer and xfer start index. + snd_linear_count = samplePairCount - lpos; + + // clamp snd_linear_count to be only as many samplepairs premixed + if ( snd_linear_count > endtime - lpaintedtime ) + { + // endtime - lpaintedtime = number of premixed sample pairs ready for xfer. + snd_linear_count = endtime - lpaintedtime; + } + + // snd_linear_count is now number of mono 16 bit samples (L and R) to xfer. + snd_linear_count <<= 1; + + // write a linear blast of samples + SND_RecordBuffer(); + if ( bShouldPlaySound ) + { + // transfer 16bit samples from snd_p into snd_out, multiplying each sample by volume. + Snd_WriteLinearBlastStereo16(); + } + + // advance paintbuffer pointer + snd_p += snd_linear_count; + + // advance lpaintedtime by number of samplepairs just xfered. + lpaintedtime += (snd_linear_count>>1); + } +} + +// Transfer contents of main paintbuffer pfront out to +// device. Perform volume multiply on each sample. +void S_TransferPaintBuffer(void *pOutput, const portable_samplepair_t *pfront, int lpaintedtime, int endtime) +{ + int out_idx; // mono sample index + int count; // number of mono samples to output + int out_mask; + int step; + int val; + int nSoundVol; + const int *p; + + if ( IsX360() ) + { + // not the right path for 360 + Assert( 0 ); + return; + } + + Assert( pOutput ); + + p = (const int *) pfront; + + count = ((endtime - lpaintedtime) * g_AudioDevice->DeviceChannels()); + + out_mask = g_AudioDevice->DeviceSampleCount() - 1; + + // 44k: remove old 22k sound support << HISPEED_DMA + // out_idx = ((paintedtime << HISPEED_DMA) * g_AudioDevice->DeviceChannels()) & out_mask; + + out_idx = (lpaintedtime * g_AudioDevice->DeviceChannels()) & out_mask; + + step = 3 - g_AudioDevice->DeviceChannels(); // mono output buffer - step 2, stereo - step 1 + nSoundVol = S_GetMasterVolume()*256; + + if (g_AudioDevice->DeviceSampleBits() == 16) + { + short *out = (short *) pOutput; + while (count--) + { + val = (*p * nSoundVol) >> 8; + p+= step; + val = CLIP(val); + + out[out_idx] = val; + out_idx = (out_idx + 1) & out_mask; + } + } + else if (g_AudioDevice->DeviceSampleBits() == 8) + { + unsigned char *out = (unsigned char *) pOutput; + while (count--) + { + val = (*p * nSoundVol) >> 8; + p+= step; + val = CLIP(val); + + out[out_idx] = (val>>8) + 128; + out_idx = (out_idx + 1) & out_mask; + } + } +} + +/* +=============================================================================== + +CHANNEL MIXING + +=============================================================================== +*/ + + +// free channel so that it may be allocated by the +// next request to play a sound. If sound is a +// word in a sentence, release the sentence. +// Works for static, dynamic, sentence and stream sounds + +void S_FreeChannel(channel_t *ch) +{ + // Don't reenter in here (can happen inside voice code). + if ( ch->flags.m_bIsFreeingChannel ) + return; + ch->flags.m_bIsFreeingChannel = true; + + SND_CloseMouth(ch); + + g_pSoundServices->OnSoundStopped( ch->guid, ch->soundsource, ch->entchannel, ch->sfx->getname() ); + + ch->flags.isSentence = false; +// Msg("End sound %s\n", ch->sfx->getname() ); + + delete ch->pMixer; + ch->pMixer = NULL; + ch->sfx = NULL; + + // zero all data in channel + g_ActiveChannels.Remove( ch ); + Q_memset(ch, 0, sizeof(channel_t)); +} + + +// Mix all channels into active paintbuffers until paintbuffer is full or 'endtime' is reached. +// endtime: time in 44khz samples to mix +// rate: ignore samples which are not natively at this rate (for multipass mixing/filtering) +// if rate == SOUND_ALL_RATES then mix all samples this pass +// flags: if SOUND_MIX_DRY, then mix only samples with channel flagged as 'dry' +// outputRate: target mix rate for all samples. Note, if outputRate = SOUND_DMA_SPEED, then +// this routine will fill the paintbuffer to endtime. Otherwise, fewer samples are mixed. +// if (endtime - paintedtime) is not aligned on boundaries of 4, +// we'll miss data if outputRate < SOUND_DMA_SPEED! +void MIX_MixChannelsToPaintbuffer( CChannelList &list, int endtime, int flags, int rate, int outputRate ) +{ + VPROF( "MixChannelsToPaintbuffer" ); + int i; + int sampleCount; + + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s c:%d %d/%d", __FUNCTION__, list.Count(), rate, outputRate ); + + // mix each channel into paintbuffer + // validate parameters + Assert( outputRate <= SOUND_DMA_SPEED ); + Assert( !((endtime - g_paintedtime) & 0x3) || (outputRate == SOUND_DMA_SPEED) ); // make sure we're not discarding data + + // 44k: try to mix this many samples at outputRate + sampleCount = ( endtime - g_paintedtime ) / ( SOUND_DMA_SPEED / outputRate ); + if ( sampleCount <= 0 ) + return; + + // Apply a global pitch shift if we're playing back a time-scaled replay + float flGlobalPitchScale = 1.0f; + +#if defined( REPLAY_ENABLED ) + extern IDemoPlayer *g_pReplayDemoPlayer; + if ( demoplayer->IsPlayingBack() && demoplayer == g_pReplayDemoPlayer ) + { + // adjust time scale if playing back demo + flGlobalPitchScale = demoplayer->GetPlaybackTimeScale(); + } +#endif + + for ( i = list.Count(); --i >= 0; ) + { + channel_t *ch = list.GetChannel( i ); + Assert( ch->sfx ); + // must never have a 'dry' and 'speaker' set - causes double mixing & double data reading + Assert ( !( ( ch->flags.bdry && ch->flags.bSpeaker ) || ( ch->flags.bdry && ch->special_dsp != 0 ) ) ); + + // if mixing with SOUND_MIX_DRY flag, ignore (don't even load) all channels not flagged as 'dry' + if ( flags == SOUND_MIX_DRY ) + { + if ( !ch->flags.bdry ) + continue; + } + + // if mixing with SOUND_MIX_WET flag, ignore (don't even load) all channels flagged as 'dry' or 'speaker' + if ( flags == SOUND_MIX_WET ) + { + if ( ch->flags.bdry || ch->flags.bSpeaker || ch->special_dsp != 0 ) + continue; + } + + // if mixing with SOUND_MIX_SPEAKER flag, ignore (don't even load) all channels not flagged as 'speaker' + if ( flags == SOUND_MIX_SPEAKER ) + { + if ( !ch->flags.bSpeaker ) + continue; + } + + // if mixing with SOUND_MIX_SPEAKER flag, ignore (don't even load) all channels not flagged as 'speaker' + if ( flags == SOUND_MIX_SPECIAL_DSP ) + { + if ( ch->special_dsp == 0 ) + continue; + } + + // multipass mixing - only mix samples of specified sample rate + switch ( rate ) + { + case SOUND_11k: + case SOUND_22k: + case SOUND_44k: + if ( rate != ch->sfx->pSource->SampleRate() ) + continue; + break; + default: + case SOUND_ALL_RATES: + break; + } + + // Tracker 20771, if breen is speaking through the monitor, the client doesn't have an entity + // for the "soundsource" but we still need the lipsync to pause if the game is paused. Therefore + // I changed SND_IsMouth to look for any .wav on any channels which has sentence data + bool bIsMouth = SND_IsMouth(ch); + bool bShouldPause = IsX360() ? !ch->sfx->m_bIsUISound : bIsMouth; + + // Tracker 14637: Pausing the game pauses voice sounds, but not other sounds... + if ( bShouldPause && g_pSoundServices->IsGamePaused() ) + { + continue; + } + + if ( bIsMouth ) + { + if ( ( ch->soundsource == SOUND_FROM_UI_PANEL ) || entitylist->GetClientEntity(ch->soundsource) || + ( ch->flags.bSpeaker && entitylist->GetClientEntity( ch->speakerentity ) ) ) + { + // UNDONE: recode this as a member function of CAudioMixer + SND_MoveMouth8(ch, ch->sfx->pSource, sampleCount); + } + } + + // mix channel to all active paintbuffers: + // mix 'dry' sounds only to dry paintbuffer. + // mix 'speaker' sounds only to speaker paintbuffer. + // mix all other sounds between room, facing & facingaway paintbuffers + // NOTE: must be called once per channel only - consecutive calls retrieve additional data. + float flPitch = ch->pitch; + ch->pitch *= flGlobalPitchScale; + + if (list.IsQuashed(i)) + { + // If the sound has been silenced as a performance heuristic, quash it. + ch->pMixer->SkipSamples( ch, sampleCount, outputRate, 0 ); + // DevMsg("Quashed channel %d (%s)\n", i, ch->sfx->GetFileName()); + } + else + { + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "MixDataToDevice" ); + ch->pMixer->MixDataToDevice( g_AudioDevice, ch, sampleCount, outputRate, 0 ); + } + + // restore to original pitch settings + ch->pitch = flPitch; + + if ( !ch->pMixer->ShouldContinueMixing() ) + { + S_FreeChannel( ch ); + list.RemoveChannelFromList(i); + } + if ( (ch->nFreeChannelAtSampleTime > 0 && (int)ch->nFreeChannelAtSampleTime <= endtime) ) + { + S_FreeChannel( ch ); + list.RemoveChannelFromList(i); + } + } +} + +// pass in index -1...count+2, return pointer to source sample in either paintbuffer or delay buffer +inline portable_samplepair_t * S_GetNextpFilter(int i, portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem) +{ + // The delay buffer is assumed to precede the paintbuffer by 6 duplicated samples + if (i == -1) + return (&(pfiltermem[0])); + if (i == 0) + return (&(pfiltermem[1])); + if (i == 1) + return (&(pfiltermem[2])); + + // return from paintbuffer, where samples are doubled. + // even samples are to be replaced with interpolated value. + + return (&(pbuffer[(i-2)*2 + 1])); +} + +// pass forward over passed in buffer and cubic interpolate all odd samples +// pbuffer: buffer to filter (in place) +// prevfilter: filter memory. NOTE: this must match the filtertype ie: filtercubic[] for FILTERTYPE_CUBIC +// if NULL then perform no filtering. UNDONE: should have a filter memory array type +// count: how many samples to upsample. will become count*2 samples in buffer, in place. + +void S_Interpolate2xCubic( portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem, int cfltmem, int count ) +{ + +// implement cubic interpolation on 2x upsampled buffer. Effectively delays buffer contents by 2 samples. +// pbuffer: contains samples at 0, 2, 4, 6... +// temppaintbuffer is temp buffer, of same or larger size than a paintbuffer, used to store processed values +// count: number of samples to process in buffer ie: how many samples at 0, 2, 4, 6... + +// finpos is the fractional, inpos the integer part. +// finpos = 0.5 for upsampling by 2x +// inpos is the position of the sample + +// xm1 = x [inpos - 1]; +// x0 = x [inpos + 0]; +// x1 = x [inpos + 1]; +// x2 = x [inpos + 2]; +// a = (3 * (x0-x1) - xm1 + x2) / 2; +// b = 2*x1 + xm1 - (5*x0 + x2) / 2; +// c = (x1 - xm1) / 2; +// y [outpos] = (((a * finpos) + b) * finpos + c) * finpos + x0; + + int i, upCount = count << 1; + int a, b, c; + int xm1, x0, x1, x2; + portable_samplepair_t *psamp0; + portable_samplepair_t *psamp1; + portable_samplepair_t *psamp2; + portable_samplepair_t *psamp3; + int outpos = 0; + + Assert (upCount <= PAINTBUFFER_SIZE); + + // pfiltermem holds 6 samples from previous buffer pass + + // process 'count' samples + + for ( i = 0; i < count; i++) + { + + // get source sample pointer + + psamp0 = S_GetNextpFilter(i-1, pbuffer, pfiltermem); + psamp1 = S_GetNextpFilter(i, pbuffer, pfiltermem); + psamp2 = S_GetNextpFilter(i+1, pbuffer, pfiltermem); + psamp3 = S_GetNextpFilter(i+2, pbuffer, pfiltermem); + + // write out original sample to interpolation buffer + + g_temppaintbuffer[outpos++] = *psamp1; + + // get all left samples for interpolation window + + xm1 = psamp0->left; + x0 = psamp1->left; + x1 = psamp2->left; + x2 = psamp3->left; + + // interpolate + + a = (3 * (x0-x1) - xm1 + x2) / 2; + b = 2*x1 + xm1 - (5*x0 + x2) / 2; + c = (x1 - xm1) / 2; + + // write out interpolated sample + + g_temppaintbuffer[outpos].left = a/8 + b/4 + c/2 + x0; + + // get all right samples for window + + xm1 = psamp0->right; + x0 = psamp1->right; + x1 = psamp2->right; + x2 = psamp3->right; + + // interpolate + + a = (3 * (x0-x1) - xm1 + x2) / 2; + b = 2*x1 + xm1 - (5*x0 + x2) / 2; + c = (x1 - xm1) / 2; + + // write out interpolated sample, increment output counter + g_temppaintbuffer[outpos++].right = a/8 + b/4 + c/2 + x0; + + Assert( outpos <= TEMP_COPY_BUFFER_SIZE ); + } + + Assert(cfltmem >= 3); + + // save last 3 samples from paintbuffer + + pfiltermem[0] = pbuffer[upCount - 5]; + pfiltermem[1] = pbuffer[upCount - 3]; + pfiltermem[2] = pbuffer[upCount - 1]; + + // copy temppaintbuffer back into paintbuffer + + for (i = 0; i < upCount; i++) + pbuffer[i] = g_temppaintbuffer[i]; +} + +// pass forward over passed in buffer and linearly interpolate all odd samples +// pbuffer: buffer to filter (in place) +// prevfilter: filter memory. NOTE: this must match the filtertype ie: filterlinear[] for FILTERTYPE_LINEAR +// if NULL then perform no filtering. +// count: how many samples to upsample. will become count*2 samples in buffer, in place. + +void S_Interpolate2xLinear( portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem, int cfltmem, int count ) +{ + int i, upCount = count<<1; + + Assert (upCount <= PAINTBUFFER_SIZE); + Assert (cfltmem >= 1); + + // use interpolation value from previous mix + + pbuffer[0].left = (pfiltermem->left + pbuffer[0].left) >> 1; + pbuffer[0].right = (pfiltermem->right + pbuffer[0].right) >> 1; + + for ( i = 2; i < upCount; i+=2) + { + // use linear interpolation for upsampling + + pbuffer[i].left = (pbuffer[i].left + pbuffer[i-1].left) >> 1; + pbuffer[i].right = (pbuffer[i].right + pbuffer[i-1].right) >> 1; + } + + // save last value to be played out in buffer + + *pfiltermem = pbuffer[upCount - 1]; +} + + +// Optimized routine. 2.27X faster than the above routine +void S_Interpolate2xLinear_2( int count, portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem, int cfltmem ) +{ + Assert (cfltmem >= 1); + + int sample = count-1; + int end = (count*2)-1; + portable_samplepair_t *pwrite = &pbuffer[end]; + portable_samplepair_t *pread = &pbuffer[sample]; + portable_samplepair_t last = pread[0]; + pread--; + + // PERFORMANCE: Unroll the loop 8 times. This improves speed quite a bit + for ( ;sample >= 8; sample -= 8 ) + { + pwrite[0] = last; + pwrite[-1].left = (pread[0].left + last.left)>>1; + pwrite[-1].right = (pread[0].right + last.right)>>1; + last = pread[0]; + + pwrite[-2] = last; + pwrite[-3].left = (pread[-1].left + last.left)>>1; + pwrite[-3].right = (pread[-1].right + last.right)>>1; + last = pread[-1]; + + pwrite[-4] = last; + pwrite[-5].left = (pread[-2].left + last.left)>>1; + pwrite[-5].right = (pread[-2].right + last.right)>>1; + last = pread[-2]; + + pwrite[-6] = last; + pwrite[-7].left = (pread[-3].left + last.left)>>1; + pwrite[-7].right = (pread[-3].right + last.right)>>1; + last = pread[-3]; + + pwrite[-8] = last; + pwrite[-9].left = (pread[-4].left + last.left)>>1; + pwrite[-9].right = (pread[-4].right + last.right)>>1; + last = pread[-4]; + + pwrite[-10] = last; + pwrite[-11].left = (pread[-5].left + last.left)>>1; + pwrite[-11].right = (pread[-5].right + last.right)>>1; + last = pread[-5]; + + pwrite[-12] = last; + pwrite[-13].left = (pread[-6].left + last.left)>>1; + pwrite[-13].right = (pread[-6].right + last.right)>>1; + last = pread[-6]; + + pwrite[-14] = last; + pwrite[-15].left = (pread[-7].left + last.left)>>1; + pwrite[-15].right = (pread[-7].right + last.right)>>1; + last = pread[-7]; + + pread -= 8; + pwrite -= 16; + } + while ( pread >= pbuffer ) + { + pwrite[0] = last; + pwrite[-1].left = (pread[0].left + last.left)>>1; + pwrite[-1].right = (pread[0].right + last.right)>>1; + last = pread[0]; + pread--; + pwrite-=2; + } + pbuffer[1] = last; + pbuffer[0].left = (pfiltermem->left + last.left) >> 1; + pbuffer[0].right = (pfiltermem->right + last.right) >> 1; + *pfiltermem = pbuffer[end]; +} + + +// upsample by 2x, optionally using interpolation +// count: how many samples to upsample. will become count*2 samples in buffer, in place. +// pbuffer: buffer to upsample into (in place) +// pfiltermem: filter memory. NOTE: this must match the filtertype ie: filterlinear[] for FILTERTYPE_LINEAR +// if NULL then perform no filtering. +// cfltmem: max number of sample pairs filter can use +// filtertype: FILTERTYPE_NONE, _LINEAR, _CUBIC etc. Must match prevfilter. +void S_MixBufferUpsample2x( int count, portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem, int cfltmem, int filtertype ) +{ + // JAY: Optimized this routine. Test then remove old routine. + // NOTE: Has been proven equivalent by comparing output. + if ( filtertype == FILTERTYPE_LINEAR ) + { + S_Interpolate2xLinear_2( count, pbuffer, pfiltermem, cfltmem ); + return; + } + int i, j, upCount = count<<1; + + // reverse through buffer, duplicating contents for 'count' samples + + for (i = upCount - 1, j = count - 1; j >= 0; i-=2, j--) + { + pbuffer[i] = pbuffer[j]; + pbuffer[i-1] = pbuffer[j]; + } + + // pass forward through buffer, interpolate all even slots + + switch (filtertype) + { + default: + break; + case FILTERTYPE_LINEAR: + S_Interpolate2xLinear(pbuffer, pfiltermem, cfltmem, count); + break; + case FILTERTYPE_CUBIC: + S_Interpolate2xCubic(pbuffer, pfiltermem, cfltmem, count); + break; + } +} + +//=============================================================================== +// PAINTBUFFER ROUTINES +//=============================================================================== + + +// Set current paintbuffer to pbuf. +// The set paintbuffer is used by all subsequent mixing, upsampling and dsp routines. +// Also sets the rear paintbuffer if paintbuffer has fsurround true. +// (otherwise, rearpaintbuffer is NULL) + +void MIX_SetCurrentPaintbuffer(int ipaintbuffer) +{ + // set front and rear paintbuffer + + Assert(ipaintbuffer < g_paintBuffers.Count()); + + g_curpaintbuffer = g_paintBuffers[ipaintbuffer].pbuf; + + if ( g_paintBuffers[ipaintbuffer].fsurround ) + { + g_currearpaintbuffer = g_paintBuffers[ipaintbuffer].pbufrear; + + g_curcenterpaintbuffer = NULL; + + if ( g_paintBuffers[ipaintbuffer].fsurround_center ) + g_curcenterpaintbuffer = g_paintBuffers[ipaintbuffer].pbufcenter; + } + else + { + g_currearpaintbuffer = NULL; + g_curcenterpaintbuffer = NULL; + } + + Assert(g_curpaintbuffer != NULL); +} + +// return index to current paintbuffer + +int MIX_GetCurrentPaintbufferIndex( void ) +{ + int i; + + for ( i = 0; i < g_paintBuffers.Count(); i++ ) + { + if (g_curpaintbuffer == g_paintBuffers[i].pbuf) + return i; + } + + return 0; +} + + +// return pointer to current paintbuffer struct + +paintbuffer_t *MIX_GetCurrentPaintbufferPtr( void ) +{ + int ipaint = MIX_GetCurrentPaintbufferIndex(); + + Assert( ipaint < g_paintBuffers.Count() ); + + return &g_paintBuffers[ipaint]; +} + + +// return pointer to front paintbuffer pbuf, given index + +inline portable_samplepair_t *MIX_GetPFrontFromIPaint(int ipaintbuffer) +{ + return g_paintBuffers[ipaintbuffer].pbuf; +} + +paintbuffer_t *MIX_GetPPaintFromIPaint( int ipaintbuffer ) +{ + Assert( ipaintbuffer < g_paintBuffers.Count() ); + + return &g_paintBuffers[ipaintbuffer]; +} + + +// return pointer to rear buffer, given index. +// returns null if fsurround is false; + +inline portable_samplepair_t *MIX_GetPRearFromIPaint(int ipaintbuffer) +{ + if ( g_paintBuffers[ipaintbuffer].fsurround ) + return g_paintBuffers[ipaintbuffer].pbufrear; + + return NULL; +} + +// return pointer to center buffer, given index. +// returns null if fsurround_center is false; + +inline portable_samplepair_t *MIX_GetPCenterFromIPaint(int ipaintbuffer) +{ + if ( g_paintBuffers[ipaintbuffer].fsurround_center ) + return g_paintBuffers[ipaintbuffer].pbufcenter; + + return NULL; +} + +// return index to paintbuffer, given buffer pointer + +inline int MIX_GetIPaintFromPFront( portable_samplepair_t *pbuf ) +{ + int i; + + for ( i = 0; i < g_paintBuffers.Count(); i++ ) + { + if ( pbuf == g_paintBuffers[i].pbuf ) + return i; + } + + return 0; +} + +// return pointer to paintbuffer struct, given ptr to buffer data + +inline paintbuffer_t *MIX_GetPPaintFromPFront( portable_samplepair_t *pbuf ) +{ + int i; + i = MIX_GetIPaintFromPFront( pbuf ); + + return &g_paintBuffers[i]; +} + + +// up convert mono buffer to full surround + +inline void MIX_ConvertBufferToSurround( int ipaintbuffer ) +{ + paintbuffer_t *ppaint = &g_paintBuffers[ipaintbuffer]; + + // duplicate channel data as needed + + if ( g_AudioDevice->IsSurround() ) + { + // set buffer flags + + ppaint->fsurround = g_AudioDevice->IsSurround(); + ppaint->fsurround_center = g_AudioDevice->IsSurroundCenter(); + + portable_samplepair_t *pfront = MIX_GetPFrontFromIPaint( ipaintbuffer ); + portable_samplepair_t *prear = MIX_GetPRearFromIPaint( ipaintbuffer ); + portable_samplepair_t *pcenter = MIX_GetPCenterFromIPaint( ipaintbuffer ); + + // copy front to rear + Q_memcpy(prear, pfront, sizeof(portable_samplepair_t) * PAINTBUFFER_SIZE); + + // copy front to center + if ( g_AudioDevice->IsSurroundCenter() ) + Q_memcpy(pcenter, pfront, sizeof(portable_samplepair_t) * PAINTBUFFER_SIZE); + } +} + +// Activate a paintbuffer. All active paintbuffers are mixed in parallel within +// MIX_MixChannelsToPaintbuffer, according to flags + +inline void MIX_ActivatePaintbuffer(int ipaintbuffer) +{ + Assert( ipaintbuffer < g_paintBuffers.Count() ); + g_paintBuffers[ipaintbuffer].factive = true; +} + +// Don't mix into this paintbuffer + +inline void MIX_DeactivatePaintbuffer(int ipaintbuffer) +{ + Assert( ipaintbuffer < g_paintBuffers.Count() ); + g_paintBuffers[ipaintbuffer].factive = false; +} + +// Don't mix into any paintbuffers + +inline void MIX_DeactivateAllPaintbuffers(void) +{ + int i; + for ( i = 0; i < g_paintBuffers.Count(); i++ ) + g_paintBuffers[i].factive = false; +} + +// set upsampling filter indexes back to 0 + +inline void MIX_ResetPaintbufferFilterCounters( void ) + +{ + int i; + for ( i = 0; i < g_paintBuffers.Count(); i++ ) + g_paintBuffers[i].ifilter = 0; +} + +inline void MIX_ResetPaintbufferFilterCounter( int ipaintbuffer ) +{ + Assert ( ipaintbuffer < g_paintBuffers.Count() ); + g_paintBuffers[ipaintbuffer].ifilter = 0; +} + +// Change paintbuffer's flags + +inline void MIX_SetPaintbufferFlags(int ipaintbuffer, int flags) +{ + Assert( ipaintbuffer < g_paintBuffers.Count() ); + g_paintBuffers[ipaintbuffer].flags = flags; +} + + +// zero out all paintbuffers + +void MIX_ClearAllPaintBuffers( int SampleCount, bool clearFilters ) +{ + // g_paintBuffers can be NULL with -nosound + if ( g_paintBuffers.Count() <= 0 ) + { + return; + } + + int i; + int count = min(SampleCount, PAINTBUFFER_SIZE); + + // zero out all paintbuffer data (ignore sampleCount) + + for ( i = 0; i < g_paintBuffers.Count(); i++ ) + { + if (g_paintBuffers[i].pbuf != NULL) + Q_memset(g_paintBuffers[i].pbuf, 0, (count+1) * sizeof(portable_samplepair_t)); + + if (g_paintBuffers[i].pbufrear != NULL) + Q_memset(g_paintBuffers[i].pbufrear, 0, (count+1) * sizeof(portable_samplepair_t)); + + if (g_paintBuffers[i].pbufcenter != NULL) + Q_memset(g_paintBuffers[i].pbufcenter, 0, (count+1) * sizeof(portable_samplepair_t)); + + if ( clearFilters ) + { + Q_memset( g_paintBuffers[i].fltmem, 0, sizeof(g_paintBuffers[i].fltmem) ); + Q_memset( g_paintBuffers[i].fltmemrear, 0, sizeof(g_paintBuffers[i].fltmemrear) ); + Q_memset( g_paintBuffers[i].fltmemcenter, 0, sizeof(g_paintBuffers[i].fltmemcenter) ); + } + } + + if ( clearFilters ) + { + MIX_ResetPaintbufferFilterCounters(); + } +} + +#define SWAP(a,b,t) {(t) = (a); (a) = (b); (b) = (t);} +#define AVG(a,b) (((a) + (b)) >> 1 ) +#define AVG4(a,b,c,d) (((a) + (b) + (c) + (d)) >> 2 ) + +// Synthesize center channel from left/right values (average). +// Currently just averages, but could actually remove +// the center signal from the l/r channels... + +inline void MIX_CenterFromLeftRight( int *pl, int *pr, int *pc ) +{ + int l = *pl; + int r = *pr; + int c = 0; + + + c = (l + r) / 2; + +/* + l = l - c/2; + r = r - c/2; + + if (l < 0) + { + l = 0; + r += (-l); + c += (-l); + } + else if (r < 0) + { + r = 0; + l += (-r); + c += (-r); + } +*/ + *pc = c; +// *pl = l; +// *pr = r; +} + +// mixes pbuf1 + pbuf2 into pbuf3, count samples +// fgain is output gain 0-1.0 +// NOTE: pbuf3 may equal pbuf1 or pbuf2! + +// mixing algorithms: + +// destination 2ch: +// pb1 2ch + pb2 2ch -> pb3 2ch +// pb1 (4ch->2ch) + pb2 2ch -> pb3 2ch +// pb1 2ch + pb2 (4ch->2ch) -> pb3 2ch +// pb1 (4ch->2ch) + pb2 (4ch->2ch) -> pb3 2ch + +// destination 4ch: +// pb1 4ch + pb2 4ch -> pb3 4ch +// pb1 (2ch->4ch) + pb2 4ch -> pb3 4ch +// pb1 4ch + pb2 (2ch->4ch) -> pb3 4ch +// pb1 (2ch->4ch) + pb2 (2ch->4ch) -> pb3 4ch + +// if all buffers are 4 or 5 ch surround, mix rear & center channels into ibuf3 as well. + +// NOTE: for performance, conversion and mixing are done in a single pass instead of +// a two pass channel convert + mix scheme. + +void MIX_MixPaintbuffers(int ibuf1, int ibuf2, int ibuf3, int count, float fgain_out) +{ + VPROF("Mixpaintbuffers"); + int i; + portable_samplepair_t *pbuf1, *pbuf2, *pbuf3, *pbuft; + portable_samplepair_t *pbufrear1, *pbufrear2, *pbufrear3, *pbufreart; + portable_samplepair_t *pbufcenter1, *pbufcenter2, *pbufcenter3, *pbufcentert; + int cchan1, cchan2, cchan3, cchant; + int xl,xr; + int l,r,l2,r2,c, c2; + int gain_out; + + gain_out = 256 * fgain_out; + + Assert (count <= PAINTBUFFER_SIZE); + Assert (ibuf1 < g_paintBuffers.Count()); + Assert (ibuf2 < g_paintBuffers.Count()); + Assert (ibuf3 < g_paintBuffers.Count()); + + pbuf1 = g_paintBuffers[ibuf1].pbuf; + pbuf2 = g_paintBuffers[ibuf2].pbuf; + pbuf3 = g_paintBuffers[ibuf3].pbuf; + + pbufrear1 = g_paintBuffers[ibuf1].pbufrear; + pbufrear2 = g_paintBuffers[ibuf2].pbufrear; + pbufrear3 = g_paintBuffers[ibuf3].pbufrear; + + pbufcenter1 = g_paintBuffers[ibuf1].pbufcenter; + pbufcenter2 = g_paintBuffers[ibuf2].pbufcenter; + pbufcenter3 = g_paintBuffers[ibuf3].pbufcenter; + + cchan1 = 2 + (g_paintBuffers[ibuf1].fsurround ? 2 : 0) + (g_paintBuffers[ibuf1].fsurround_center ? 1 : 0); + cchan2 = 2 + (g_paintBuffers[ibuf2].fsurround ? 2 : 0) + (g_paintBuffers[ibuf2].fsurround_center ? 1 : 0); + cchan3 = 2 + (g_paintBuffers[ibuf3].fsurround ? 2 : 0) + (g_paintBuffers[ibuf3].fsurround_center ? 1 : 0); + + // make sure pbuf1 always has fewer or equal channels than pbuf2 + // NOTE: pbuf3 may equal pbuf1 or pbuf2! + + if ( cchan2 < cchan1 ) + { + SWAP( cchan1, cchan2, cchant ); + SWAP( pbuf1, pbuf2, pbuft ); + SWAP( pbufrear1, pbufrear2, pbufreart ); + SWAP( pbufcenter1, pbufcenter2, pbufcentert); + } + + + // UNDONE: implement fast mixing routines for each of the following sections + + // destination buffer stereo - average n chans down to stereo + + if ( cchan3 == 2 ) + { + // destination 2ch: + // pb1 2ch + pb2 2ch -> pb3 2ch + // pb1 2ch + pb2 (4ch->2ch) -> pb3 2ch + // pb1 (4ch->2ch) + pb2 (4ch->2ch) -> pb3 2ch + + if ( cchan1 == 2 && cchan2 == 2 ) + { + // mix front channels + + for (i = 0; i < count; i++) + { + pbuf3[i].left = pbuf1[i].left + pbuf2[i].left; + pbuf3[i].right = pbuf1[i].right + pbuf2[i].right; + } + goto gain2ch; + } + + if ( cchan1 == 2 && cchan2 == 4 ) + { + // avg rear chan l/r + + for (i = 0; i < count; i++) + { + pbuf3[i].left = pbuf1[i].left + AVG( pbuf2[i].left, pbufrear2[i].left ); + pbuf3[i].right = pbuf1[i].right + AVG( pbuf2[i].right, pbufrear2[i].right ); + } + goto gain2ch; + } + + if ( cchan1 == 4 && cchan2 == 4 ) + { + // avg rear chan l/r + + for (i = 0; i < count; i++) + { + pbuf3[i].left = AVG( pbuf1[i].left, pbufrear1[i].left) + AVG( pbuf2[i].left, pbufrear2[i].left ); + pbuf3[i].right = AVG( pbuf1[i].right, pbufrear1[i].right) + AVG( pbuf2[i].right, pbufrear2[i].right ); + } + goto gain2ch; + } + + if ( cchan1 == 2 && cchan2 == 5 ) + { + // avg rear chan l/r + center split into left/right + + for (i = 0; i < count; i++) + { + l = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + pbuf3[i].left = pbuf1[i].left + AVG( l, pbufrear2[i].left ); + pbuf3[i].right = pbuf1[i].right + AVG( r, pbufrear2[i].right ); + } + goto gain2ch; + } + + if ( cchan1 == 4 && cchan2 == 5) + { + for (i = 0; i < count; i++) + { + l = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + pbuf3[i].left = AVG( pbuf1[i].left, pbufrear1[i].left) + AVG( l, pbufrear2[i].left ); + pbuf3[i].right = AVG( pbuf1[i].right, pbufrear1[i].right) + AVG( r, pbufrear2[i].right ); + } + goto gain2ch; + } + + if ( cchan1 == 5 && cchan2 == 5) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left + ((pbufcenter1[i].left) >> 1); + r = pbuf1[i].right + ((pbufcenter1[i].left) >> 1); + + l2 = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r2 = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + pbuf3[i].left = AVG( l, pbufrear1[i].left) + AVG( l2, pbufrear2[i].left ); + pbuf3[i].right = AVG( r, pbufrear1[i].right) + AVG( r2, pbufrear2[i].right ); + } goto gain2ch; + } + + } + + // destination buffer quad - duplicate n chans up to quad + + if ( cchan3 == 4 ) + { + + // pb1 4ch + pb2 4ch -> pb3 4ch + // pb1 (2ch->4ch) + pb2 4ch -> pb3 4ch + // pb1 (2ch->4ch) + pb2 (2ch->4ch) -> pb3 4ch + + if ( cchan1 == 4 && cchan2 == 4) + { + // mix front -> front, rear -> rear + + for (i = 0; i < count; i++) + { + pbuf3[i].left = pbuf1[i].left + pbuf2[i].left; + pbuf3[i].right = pbuf1[i].right + pbuf2[i].right; + + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + } + goto gain4ch; + } + + if ( cchan1 == 2 && cchan2 == 4) + { + + for (i = 0; i < count; i++) + { + // split 2 ch left -> front left, rear left + // split 2 ch right -> front right, rear right + + xl = pbuf1[i].left; + xr = pbuf1[i].right; + + pbuf3[i].left = xl + pbuf2[i].left; + pbuf3[i].right = xr + pbuf2[i].right; + + pbufrear3[i].left = xl + pbufrear2[i].left; + pbufrear3[i].right = xr + pbufrear2[i].right; + } + goto gain4ch; + } + + if ( cchan1 == 2 && cchan2 == 2) + { + // mix l,r, split into front l, front r + + for (i = 0; i < count; i++) + { + xl = pbuf1[i].left + pbuf2[i].left; + xr = pbuf1[i].right + pbuf2[i].right; + + pbufrear3[i].left = pbuf3[i].left = xl; + pbufrear3[i].right = pbuf3[i].right = xr; + } + goto gain4ch; + } + + + if ( cchan1 == 2 && cchan2 == 5 ) + { + for (i = 0; i < count; i++) + { + // split center of chan2 into left/right + + l2 = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r2 = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + xl = pbuf1[i].left; + xr = pbuf1[i].right; + + pbuf3[i].left = xl + l2; + pbuf3[i].right = xr + r2; + + pbufrear3[i].left = xl + pbufrear2[i].left; + pbufrear3[i].right = xr + pbufrear2[i].right; + } + goto gain4ch; + } + + if ( cchan1 == 4 && cchan2 == 5) + { + + for (i = 0; i < count; i++) + { + l2 = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r2 = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + pbuf3[i].left = pbuf1[i].left + l2; + pbuf3[i].right = pbuf1[i].right + r2; + + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + } + goto gain4ch; + } + + if ( cchan1 == 5 && cchan2 == 5 ) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left + ((pbufcenter1[i].left) >> 1); + r = pbuf1[i].right + ((pbufcenter1[i].left) >> 1); + + l2 = pbuf2[i].left + ((pbufcenter2[i].left) >> 1); + r2 = pbuf2[i].right + ((pbufcenter2[i].left) >> 1); + + pbuf3[i].left = l + l2; + pbuf3[i].right = r + r2; + + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + } + goto gain4ch; + } + } + + // 5 channel destination + + if (cchan3 == 5) + { + // up convert from 2 or 4 ch buffer to 5 ch buffer: + // center channel is synthesized from front left, front right + + if (cchan1 == 2 && cchan2 == 2) + { + for (i = 0; i < count; i++) + { + // split 2 ch left -> front left, center, rear left + // split 2 ch right -> front right, center, rear right + + l = pbuf1[i].left; + r = pbuf1[i].right; + + MIX_CenterFromLeftRight(&l, &r, &c); + + l2 = pbuf2[i].left; + r2 = pbuf2[i].right; + + MIX_CenterFromLeftRight(&l2, &r2, &c2); + + pbuf3[i].left = l + l2; + pbuf3[i].right = r + r2; + + pbufrear3[i].left = pbuf1[i].left + pbuf2[i].left; + pbufrear3[i].right = pbuf1[i].right + pbuf2[i].right; + + pbufcenter3[i].left = c + c2; + } + goto gain5ch; + } + + if (cchan1 == 2 && cchan2 == 4) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left; + r = pbuf1[i].right; + + MIX_CenterFromLeftRight(&l, &r, &c); + + l2 = pbuf2[i].left; + r2 = pbuf2[i].right; + + MIX_CenterFromLeftRight(&l2, &r2, &c2); + + pbuf3[i].left = l + l2; + pbuf3[i].right = r + r2; + + pbufrear3[i].left = pbuf1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbuf1[i].right + pbufrear2[i].right; + + pbufcenter3[i].left = c + c2; + } + goto gain5ch; + } + + if (cchan1 == 2 && cchan2 == 5) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left; + r = pbuf1[i].right; + + MIX_CenterFromLeftRight(&l, &r, &c); + + pbuf3[i].left = l + pbuf2[i].left; + pbuf3[i].right = r + pbuf2[i].right; + + pbufrear3[i].left = pbuf1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbuf1[i].right + pbufrear2[i].right; + + pbufcenter3[i].left = c + pbufcenter2[i].left; + } + goto gain5ch; + } + + if (cchan1 == 4 && cchan2 == 4) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left; + r = pbuf1[i].right; + + MIX_CenterFromLeftRight(&l, &r, &c); + + l2 = pbuf2[i].left; + r2 = pbuf2[i].right; + + MIX_CenterFromLeftRight(&l2, &r2, &c2); + + pbuf3[i].left = l + l2; + pbuf3[i].right = r + r2; + + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + + pbufcenter3[i].left = c + c2; + } + goto gain5ch; + } + + + if (cchan1 == 4 && cchan2 == 5) + { + for (i = 0; i < count; i++) + { + l = pbuf1[i].left; + r = pbuf1[i].right; + + MIX_CenterFromLeftRight(&l, &r, &c); + + pbuf3[i].left = l + pbuf2[i].left; + pbuf3[i].right = r + pbuf2[i].right; + + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + + pbufcenter3[i].left = c + pbufcenter2[i].left; + } + goto gain5ch; + } + + if ( cchan2 == 5 && cchan1 == 5 ) + { + for (i = 0; i < count; i++) + { + pbuf3[i].left = pbuf1[i].left + pbuf2[i].left; + pbuf3[i].right = pbuf1[i].right + pbuf2[i].right; + pbufrear3[i].left = pbufrear1[i].left + pbufrear2[i].left; + pbufrear3[i].right = pbufrear1[i].right + pbufrear2[i].right; + pbufcenter3[i].left = pbufcenter1[i].left + pbufcenter2[i].left; + } + goto gain5ch; + } + } + +gain2ch: + if ( gain_out == 256) // KDB: perf + return; + + for (i = 0; i < count; i++) + { + pbuf3[i].left = (pbuf3[i].left * gain_out) >> 8; + pbuf3[i].right = (pbuf3[i].right * gain_out) >> 8; + } + return; + +gain4ch: + if ( gain_out == 256) // KDB: perf + return; + + for (i = 0; i < count; i++) + { + pbuf3[i].left = (pbuf3[i].left * gain_out) >> 8; + pbuf3[i].right = (pbuf3[i].right * gain_out) >> 8; + pbufrear3[i].left = (pbufrear3[i].left * gain_out) >> 8; + pbufrear3[i].right = (pbufrear3[i].right * gain_out) >> 8; + } + return; + +gain5ch: + if ( gain_out == 256) // KDB: perf + return; + + for (i = 0; i < count; i++) + { + pbuf3[i].left = (pbuf3[i].left * gain_out) >> 8; + pbuf3[i].right = (pbuf3[i].right * gain_out) >> 8; + pbufrear3[i].left = (pbufrear3[i].left * gain_out) >> 8; + pbufrear3[i].right = (pbufrear3[i].right * gain_out) >> 8; + pbufcenter3[i].left = (pbufcenter3[i].left * gain_out) >> 8; + } + return; +} + +// multiply all values in paintbuffer by fgain + +void MIX_ScalePaintBuffer( int bufferIndex, int count, float fgain ) +{ + portable_samplepair_t *pbuf = g_paintBuffers[bufferIndex].pbuf; + portable_samplepair_t *pbufrear = g_paintBuffers[bufferIndex].pbufrear; + portable_samplepair_t *pbufcenter = g_paintBuffers[bufferIndex].pbufcenter; + + int gain = 256 * fgain; + int i; + + if (gain == 256) + return; + + if ( !g_paintBuffers[bufferIndex].fsurround ) + { + for (i = 0; i < count; i++) + { + pbuf[i].left = (pbuf[i].left * gain) >> 8; + pbuf[i].right = (pbuf[i].right * gain) >> 8; + } + } + else + { + for (i = 0; i < count; i++) + { + pbuf[i].left = (pbuf[i].left * gain) >> 8; + pbuf[i].right = (pbuf[i].right * gain) >> 8; + pbufrear[i].left = (pbufrear[i].left * gain) >> 8; + pbufrear[i].right = (pbufrear[i].right * gain) >> 8; + } + + if (g_paintBuffers[bufferIndex].fsurround_center) + { + for (i = 0; i < count; i++) + { + pbufcenter[i].left = (pbufcenter[i].left * gain) >> 8; + // pbufcenter[i].right = (pbufcenter[i].right * gain) >> 8; mono center channel + } + } + } +} + +// DEBUG peak detection values +#define _SDEBUG 1 + +#ifdef _SDEBUG +float sdebug_avg_in = 0.0; +float sdebug_in_count = 0.0; +float sdebug_avg_out = 0.0; +float sdebug_out_count = 0.0; +#define SDEBUG_TOTAL_COUNT (3*44100) +#endif // DEBUG + +// DEBUG code - get and show peak value of specified paintbuffer +// DEBUG code - ibuf is buffer index, count is # samples to test, pppeakprev stores peak + + +void SDEBUG_GetAvgValue( int ibuf, int count, float *pav ) +{ +#ifdef _SDEBUG + if (snd_showstart.GetInt() != 4 ) + return; + + float av = 0.0; + + for (int i = 0; i < count; i++) + av += (float)(abs(g_paintBuffers[ibuf].pbuf->left) + abs(g_paintBuffers[ibuf].pbuf->right))/2.0; + + *pav = av / count; +#endif // DEBUG +} + + +void SDEBUG_GetAvgIn( int ibuf, int count) +{ + float av = 0.0; + SDEBUG_GetAvgValue( ibuf, count, &av ); + + sdebug_avg_in = ((av * count ) + (sdebug_avg_in * sdebug_in_count)) / (count + sdebug_in_count); + sdebug_in_count += count; +} + +void SDEBUG_GetAvgOut( int ibuf, int count) +{ + float av = 0.0; + SDEBUG_GetAvgValue( ibuf, count, &av ); + + sdebug_avg_out = ((av * count ) + (sdebug_avg_out * sdebug_out_count)) / (count + sdebug_out_count); + sdebug_out_count += count; +} + + +void SDEBUG_ShowAvgValue() +{ +#ifdef _SDEBUG + if (sdebug_in_count > SDEBUG_TOTAL_COUNT) + { + if ((int)sdebug_avg_in > 20.0 && (int)sdebug_avg_out > 20.0) + DevMsg("dsp avg gain:%1.2f in:%1.2f out:%1.2f 1/gain:%1.2f\n", sdebug_avg_out/sdebug_avg_in, sdebug_avg_in, sdebug_avg_out, sdebug_avg_in/sdebug_avg_out); + + sdebug_avg_in = 0.0; + sdebug_avg_out = 0.0; + sdebug_in_count = 0.0; + sdebug_out_count = 0.0; + } +#endif // DEBUG +} + +// clip all values in paintbuffer to 16bit. +// if fsurround is set for paintbuffer, also process rear buffer samples + +void MIX_CompressPaintbuffer(int ipaint, int count) +{ + VPROF("CompressPaintbuffer"); + int i; + paintbuffer_t *ppaint = MIX_GetPPaintFromIPaint(ipaint); + portable_samplepair_t *pbf; + portable_samplepair_t *pbr; + portable_samplepair_t *pbc; + + pbf = ppaint->pbuf; + pbr = ppaint->pbufrear; + pbc = ppaint->pbufcenter; + + for (i = 0; i < count; i++) + { + pbf->left = CLIP(pbf->left); + pbf->right = CLIP(pbf->right); + pbf++; + } + + if ( ppaint->fsurround ) + { + Assert (pbr); + + for (i = 0; i < count; i++) + { + pbr->left = CLIP(pbr->left); + pbr->right = CLIP(pbr->right); + pbr++; + } + } + + if ( ppaint->fsurround_center ) + { + Assert (pbc); + + for (i = 0; i < count; i++) + { + pbc->left = CLIP(pbc->left); + //pbc->right = CLIP(pbc->right); mono center channel + pbc++; + } + } +} + + +// mix and upsample channels to 44khz 'ipaintbuffer' +// mix channels matching 'flags' (SOUND_MIX_DRY, SOUND_MIX_WET, SOUND_MIX_SPEAKER) into specified paintbuffer +// upsamples 11khz, 22khz channels to 44khz. + +// NOTE: only call this on channels that will be mixed into only 1 paintbuffer +// and that will not be mixed until the next mix pass! otherwise, MIX_MixChannelsToPaintbuffer +// will advance any internal pointers on mixed channels; subsequent calls will be at +// incorrect offset. + +void MIX_MixUpsampleBuffer( CChannelList &list, int ipaintbuffer, int end, int count, int flags ) +{ + VPROF("MixUpsampleBuffer"); + int ipaintcur = MIX_GetCurrentPaintbufferIndex(); // save current paintbuffer + + // reset paintbuffer upsampling filter index + MIX_ResetPaintbufferFilterCounter( ipaintbuffer ); + + // prevent other paintbuffers from being mixed + MIX_DeactivateAllPaintbuffers(); + + MIX_ActivatePaintbuffer( ipaintbuffer ); // operates on MIX_MixChannelsToPaintbuffer + MIX_SetCurrentPaintbuffer( ipaintbuffer ); // operates on MixUpSample + + // mix 11khz channels to buffer + if ( list.m_has11kChannels ) + { + MIX_MixChannelsToPaintbuffer( list, end, flags, SOUND_11k, SOUND_11k ); + + // upsample 11khz buffer by 2x + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_11k), FILTERTYPE_LINEAR ); + } + + if ( list.m_has22kChannels || list.m_has11kChannels ) + { + // mix 22khz channels to buffer + MIX_MixChannelsToPaintbuffer( list, end, flags, SOUND_22k, SOUND_22k ); + +#if (SOUND_DMA_SPEED > SOUND_22k) + // upsample 22khz buffer by 2x + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_22k), FILTERTYPE_LINEAR ); +#endif + } + + // mix 44khz channels to buffer + MIX_MixChannelsToPaintbuffer( list, end, flags, SOUND_44k, SOUND_DMA_SPEED); + + MIX_DeactivateAllPaintbuffers(); + + // restore previous paintbuffer + MIX_SetCurrentPaintbuffer( ipaintcur ); +} + +// upsample and mix sounds into final 44khz versions of the following paintbuffers: +// SOUND_BUFFER_ROOM, SOUND_BUFFER_FACING, IFACINGAWAY, SOUND_BUFFER_DRY, SOUND_BUFFER_SPEAKER, SOUND_BUFFER_SPECIALs +// dsp fx are then applied to these buffers by the caller. +// caller also remixes all into final SOUND_BUFFER_PAINT output. + +void MIX_UpsampleAllPaintbuffers( CChannelList &list, int end, int count ) +{ + VPROF( "MixUpsampleAll" ); + + // 'dry' and 'speaker' channel sounds mix 100% into their corresponding buffers + + // mix and upsample all 'dry' sounds (channels) to 44khz SOUND_BUFFER_DRY paintbuffer + + if ( list.m_hasDryChannels ) + MIX_MixUpsampleBuffer( list, SOUND_BUFFER_DRY, end, count, SOUND_MIX_DRY ); + + // mix and upsample all 'speaker' sounds (channels) to 44khz SOUND_BUFFER_SPEAKER paintbuffer + + if ( list.m_hasSpeakerChannels ) + MIX_MixUpsampleBuffer( list, SOUND_BUFFER_SPEAKER, end, count, SOUND_MIX_SPEAKER ); + + // mix and upsample all 'special dsp' sounds (channels) to 44khz SOUND_BUFFER_SPECIALs paintbuffer + + for ( int iDSP = 0; iDSP < list.m_nSpecialDSPs.Count(); ++iDSP ) + { + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + if ( pSpecialBuffer->nSpecialDSP == list.m_nSpecialDSPs[ iDSP ] && pSpecialBuffer->idsp_specialdsp != -1 ) + { + MIX_MixUpsampleBuffer( list, i, end, count, SOUND_MIX_SPECIAL_DSP ); + break; + } + } + } + + // 'room', 'facing' 'facingaway' sounds are mixed into up to 3 buffers: + + // 11khz sounds are mixed into 3 buffers based on distance from listener, and facing direction + // These buffers are room, facing, facingaway + // These 3 mixed buffers are then each upsampled to 22khz. + + // 22khz sounds are mixed into the 3 buffers based on distance from listener, and facing direction + // These 3 mixed buffers are then each upsampled to 44khz. + + // 44khz sounds are mixed into the 3 buffers based on distance from listener, and facing direction + MIX_DeactivateAllPaintbuffers(); + + // set paintbuffer upsample filter indices to 0 + MIX_ResetPaintbufferFilterCounters(); + + if ( !g_bDspOff ) + { + // only mix to roombuffer if dsp fx are on KDB: perf + MIX_ActivatePaintbuffer(SOUND_BUFFER_ROOM); // operates on MIX_MixChannelsToPaintbuffer + } + + MIX_ActivatePaintbuffer(SOUND_BUFFER_FACING); + + if ( g_bdirectionalfx ) + { + // mix to facing away buffer only if directional presets are set + + MIX_ActivatePaintbuffer(SOUND_BUFFER_FACINGAWAY); + } + + // mix 11khz sounds: + // pan sounds between 3 busses: facing, facingaway and room buffers + + MIX_MixChannelsToPaintbuffer( list, end, SOUND_MIX_WET, SOUND_11k, SOUND_11k); + + // upsample all 11khz buffers by 2x + if ( !g_bDspOff ) + { + // only upsample roombuffer if dsp fx are on KDB: perf + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_ROOM); // operates on MixUpSample + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_11k), FILTERTYPE_LINEAR ); + } + + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_FACING); + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_11k), FILTERTYPE_LINEAR ); + + if ( g_bdirectionalfx ) + { + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_FACINGAWAY); + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_11k), FILTERTYPE_LINEAR ); + } + + // mix 22khz sounds: + // pan sounds between 3 busses: facing, facingaway and room buffers + MIX_MixChannelsToPaintbuffer( list, end, SOUND_MIX_WET, SOUND_22k, SOUND_22k); + + // upsample all 22khz buffers by 2x +#if ( SOUND_DMA_SPEED > SOUND_22k ) + if ( !g_bDspOff ) + { + // only upsample roombuffer if dsp fx are on KDB: perf + + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_ROOM); + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_22k), FILTERTYPE_LINEAR ); + } + + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_FACING); + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_22k), FILTERTYPE_LINEAR ); + + if ( g_bdirectionalfx ) + { + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_FACINGAWAY); + g_AudioDevice->MixUpsample( count / (SOUND_DMA_SPEED / SOUND_22k), FILTERTYPE_LINEAR ); + } +#endif + + // mix all 44khz sounds to all active paintbuffers + MIX_MixChannelsToPaintbuffer( list, end, SOUND_MIX_WET, SOUND_44k, SOUND_DMA_SPEED); + + MIX_DeactivateAllPaintbuffers(); + + MIX_SetCurrentPaintbuffer(SOUND_BUFFER_PAINT); +} + +ConVar snd_cull_duplicates("snd_cull_duplicates","0",FCVAR_ALLOWED_IN_COMPETITIVE,"If nonzero, aggressively cull duplicate sounds during mixing. The number specifies the number of duplicates allowed to be played."); + + +// Helper class for determining whether a given channel number should be culled from +// mixing, if snd_cull_duplicates is enabled (psychoacoustic quashing). +class CChannelCullList +{ +public: + // default constructor + CChannelCullList() : m_numChans(0) {}; + + // call if you plan on culling channels - and not otherwise, it's a little expensive + // (that's why it's not in the constructor) + void Initialize( CChannelList &list ); + + // returns true if a given channel number has been marked for culling + inline bool ShouldCull( int channelNum ) + { + return (m_numChans > channelNum) ? m_bShouldCull[channelNum] : false; + } + + // an array of sound names and their volumes + // TODO: there may be a way to do this faster on 360 (eg, pad to 128bit, use SIMD) + struct sChannelVolData + { + int m_channelNum; + int m_vol; // max volume of sound. -1 means "do not cull, ever, do not even do the math" + unsigned int m_nameHash; // a unique id for a sound file + }; +protected: + sChannelVolData m_channelInfo[MAX_CHANNELS]; + + bool m_bShouldCull[MAX_CHANNELS]; // in ChannelList order, not sorted order + int m_numChans; +}; + +// comparator for qsort as used below (eg a lambda) +// returns < 0 if a should come before b, > 0 if a should come after, 0 otherwise +static int __cdecl ChannelVolComparator ( const void * a, const void * b ) +{ + // greater numbers come first. + return static_cast<const CChannelCullList::sChannelVolData *>(b)->m_vol - static_cast<const CChannelCullList::sChannelVolData *>(a)->m_vol; +} + + +void CChannelCullList::Initialize( CChannelList &list ) +{ + VPROF("CChannelCullList::Initialize"); + // First, build a sorted list of channels by decreasing volume, and by a hash of their wavname. + m_numChans = list.Count(); + + for ( int i = m_numChans - 1 ; i >= 0 ; --i ) + { + channel_t *ch = list.GetChannel(i); + m_channelInfo[i].m_channelNum = i; + if ( ch && ch->pMixer->IsReadyToMix() ) + { + m_channelInfo[i].m_vol = ChannelLoudestCurVolume(ch); + AssertMsg(m_channelInfo[i].m_vol >= 0, "Sound channel has a negative volume?"); + m_channelInfo[i].m_nameHash = (unsigned int) ch->sfx; + } + else + { + m_channelInfo[i].m_vol = -1; + m_channelInfo[i].m_nameHash = NULL; // doesn't matter + } + } + + // set the unused channels to invalid data + for ( int i = m_numChans ; i < MAX_CHANNELS ; ++i ) + { + m_channelInfo[i].m_channelNum = -1; + m_channelInfo[i].m_vol = -1; + } + + // Sort the list. + qsort( m_channelInfo, MAX_CHANNELS, sizeof(sChannelVolData), ChannelVolComparator ); + + // Then, determine if the given sound is less than the nth loudest of its hash. If so, mark its flag + // for removal. + // TODO: use an actual algorithm rather than this bogus quadratic technique. + // (I'm using it for now because we don't have convenient/fast hash table + // classes, which would be the linear-time way to deal with this). + const int cutoff = snd_cull_duplicates.GetInt(); + for ( int i = 0 ; i < m_numChans ; ++i ) // i is index in original channel list + { + channel_t *ch = list.GetChannel(i); + // for each sound, determine where it ranks in loudness + int howManyLouder = 0; + for ( int j = 0 ; + m_channelInfo[j].m_channelNum != i && m_channelInfo[j].m_vol >= 0 && j < MAX_CHANNELS ; + ++j ) + { + // j steps through the sorted list until we find ourselves: + if (m_channelInfo[j].m_nameHash == (unsigned int)(ch->sfx)) + { + // that's another channel playing this sound but louder than me + ++howManyLouder; + } + } + if (howManyLouder >= cutoff) + { + // this sound should be culled + m_bShouldCull[i] = true; + } + else + { + // this sound should not be culled + m_bShouldCull[i] = false; + } + } +} + +ConVar snd_mute_losefocus("snd_mute_losefocus", "1", FCVAR_ARCHIVE); + +// build a list of channels that will actually do mixing in this update +// remove all active channels that won't mix for some reason +void MIX_BuildChannelList( CChannelList &list ) +{ + VPROF("MIX_BuildChannelList"); + g_ActiveChannels.GetActiveChannels( list ); + list.m_nSpecialDSPs.RemoveAll(); + list.m_hasDryChannels = false; + list.m_hasSpeakerChannels = false; + list.m_has11kChannels = false; + list.m_has22kChannels = false; + list.m_has44kChannels = false; + bool delayStartServer = false; + bool delayStartClient = false; + bool bPaused = g_pSoundServices->IsGamePaused(); +#ifdef POSIX + bool bActive = g_pSoundServices->IsGameActive(); + bool bStopOnFocusLoss = !bActive && snd_mute_losefocus.GetBool(); +#endif + + CChannelCullList cullList; + if (snd_cull_duplicates.GetInt() > 0) + { + cullList.Initialize(list); + } + + // int numQuashed = 0; + for ( int i = list.Count(); --i >= 0; ) + { + channel_t *ch = list.GetChannel(i); + bool bRemove = false; + // Certain async loaded sounds lazily load into memory in the background, use this to determine + // if the sound is ready for mixing + CAudioSource *pSource = NULL; + if ( ch->pMixer->IsReadyToMix() ) + { + pSource = S_LoadSound( ch->sfx, ch ); + + // Don't mix sound data for sounds with 'zero' volume. If it's a non-looping sound, + // just remove the sound when its volume goes to zero. If it's a 'dry' channel sound (ie: music) + // then assume bZeroVolume is fade in - don't restart + + // To be 'zero' volume, all target volume and current volume values must all be less than 5 + + bool bZeroVolume = BChannelLowVolume( ch, 1 ); + + if ( !pSource || ( bZeroVolume && !pSource->IsLooped() && !ch->flags.bdry ) ) + { + // NOTE: Since we've loaded the sound, check to see if it's a sentence. Play them at zero anyway + // to keep the character's lips moving and the captions happening. + if ( !pSource || pSource->GetSentence() == NULL ) + { + S_FreeChannel( ch ); + bRemove = true; + } + } + else if ( bZeroVolume ) + { + bRemove = true; + } + // If the sound wants to stop when the game pauses, do so + if ( bPaused && SND_ShouldPause(ch) ) + { + bRemove = true; + } +#ifdef POSIX + // If we aren't the active app and the option for background audio isn't on, mute the audio + // Windows has it's own system for background muting + if ( !bRemove && bStopOnFocusLoss ) + { + bRemove = true; + + // Free up the sound channels otherwise they start filling up + if ( pSource && ( !pSource->IsLooped() && !pSource->IsStreaming() ) ) + { + S_FreeChannel( ch ); + } + + } +#endif + // On lowend, aggressively cull duplicate sounds. + if ( !bRemove && snd_cull_duplicates.GetInt() > 0 ) + { + // We can't simply remove them, because then sounds will pile up waiting to finish later. + // We need to flag them for not mixing. + list.m_quashed[i] = cullList.ShouldCull(i); + /* + if (list.m_quashed[i]) + { + numQuashed++; + // Msg("removed %i\n", i); + } + */ + } + else + { + list.m_quashed[i] = false; + } + } + else + { + bRemove = true; + } + + if ( bRemove ) + { + list.RemoveChannelFromList(i); + continue; + } + if ( ch->flags.bSpeaker ) + { + list.m_hasSpeakerChannels = true; + } + if ( ch->special_dsp != 0 ) + { + if ( list.m_nSpecialDSPs.Find( ch->special_dsp ) == -1 ) + { + list.m_nSpecialDSPs.AddToTail( ch->special_dsp ); + } + } + if ( ch->flags.bdry ) + { + list.m_hasDryChannels = true; + } + int rate = pSource->SampleRate(); + if ( rate == SOUND_11k ) + { + list.m_has11kChannels = true; + } + else if ( rate == SOUND_22k ) + { + list.m_has22kChannels = true; + } + else if ( rate == SOUND_44k ) + { + list.m_has44kChannels = true; + } + if ( ch->flags.delayed_start && !SND_IsMouth(ch) ) + { + if ( ch->flags.fromserver ) + { + delayStartServer = true; + } + else + { + delayStartClient = true; + } + } + + // get playback pitch + ch->pitch = ch->pMixer->ModifyPitch( ch->basePitch * 0.01f ); + } + // DevMsg( "%d channels quashed.\n", numQuashed ); + + // This code will resync the delay calculation clock really often + // any time there are no scheduled waves or the game is paused + // we go ahead and reset the clock + // That way the clock is only used for short periods of time + // and we need no solution for drift + if ( bPaused || (host_frametime_unbounded > host_frametime) ) + { + delayStartClient = false; + delayStartServer = false; + } + if (!delayStartServer) + { + S_SyncClockAdjust(CLOCK_SYNC_SERVER); + } + if (!delayStartClient) + { + S_SyncClockAdjust(CLOCK_SYNC_CLIENT); + } +} + +// main mixing rountine - mix up to 'endtime' samples. +// All channels are mixed in a paintbuffer and then sent to +// hardware. + +// A mix pass is performed, resulting in mixed sounds in SOUND_BUFFER_ROOM, SOUND_BUFFER_FACING, SOUND_BUFFER_FACINGAWAY, SOUND_BUFFER_DRY, SOUND_BUFFER_SPEAKER, SOUND_BUFFER_SPECIALs + + // directional sounds are panned and mixed between SOUND_BUFFER_FACING and SOUND_BUFFER_FACINGAWAY + // omnidirectional sounds are panned 100% into SOUND_BUFFER_FACING + // sound sources far from player (ie: near back of room ) are mixed in proportion to this distance + // into SOUND_BUFFER_ROOM + // sounds with ch->bSpeaker set are mixed in mono into SOUND_BUFFER_SPEAKER + // sounds with ch->bSpecialDSP set are mixed in mono into SOUND_BUFFER_SPECIALs + +// dsp_facingaway fx (2 or 4ch filtering) are then applied to the SOUND_BUFFER_FACINGAWAY +// dsp_speaker fx (1ch) are then applied to the SOUND_BUFFER_SPEAKER +// dsp_specialdsp fx (1ch) are then applied to the SOUND_BUFFER_SPECIALs +// dsp_room fx (1ch reverb) are then applied to the SOUND_BUFFER_ROOM + +// All buffers are recombined into the SOUND_BUFFER_PAINT + +// The dsp_water and dsp_player fx are applied in series to the SOUND_BUFFER_PAINT + +// Finally, the SOUND_BUFFER_DRY buffer is mixed into the SOUND_BUFFER_PAINT + +extern ConVar dsp_off; +extern ConVar snd_profile; +extern void DEBUG_StartSoundMeasure(int type, int samplecount ); +extern void DEBUG_StopSoundMeasure(int type, int samplecount ); +extern ConVar dsp_enhance_stereo; + +extern ConVar dsp_volume; +extern ConVar dsp_vol_5ch; +extern ConVar dsp_vol_4ch; +extern ConVar dsp_vol_2ch; + +extern void MXR_SetCurrentSoundMixer( const char *szsoundmixer ); +extern ConVar snd_soundmixer; + +void MIX_PaintChannels( int endtime, bool bIsUnderwater ) +{ + VPROF("MIX_PaintChannels"); + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); + + int end; + int count; + bool b_spatial_delays = dsp_enhance_stereo.GetInt() != 0 ? true : false; + bool room_fsurround_sav; + bool room_fsurround_center_sav; + paintbuffer_t *proom = MIX_GetPPaintFromIPaint(SOUND_BUFFER_ROOM); + + CheckNewDspPresets(); + + MXR_SetCurrentSoundMixer( snd_soundmixer.GetString() ); + + // dsp performance tuning + + g_snd_profile_type = snd_profile.GetInt(); + + // dsp_off is true if no dsp processing is to run + // directional dsp processing is enabled if dsp_facingaway is non-zero + + g_bDspOff = dsp_off.GetInt() ? 1 : 0; + CChannelList list; + + MIX_BuildChannelList(list); + + // get master dsp volume + g_dsp_volume = dsp_volume.GetFloat(); + + // attenuate master dsp volume by 2,4 or 5 ch settings + if ( g_AudioDevice->IsSurround() ) + { + g_dsp_volume *= ( g_AudioDevice->IsSurroundCenter() ? dsp_vol_5ch.GetFloat() : dsp_vol_4ch.GetFloat() ); + } + else + { + g_dsp_volume *= dsp_vol_2ch.GetFloat(); + } + + if ( !g_bDspOff ) + { + g_bdirectionalfx = dsp_facingaway.GetInt() ? 1 : 0; + } + else + { + g_bdirectionalfx = 0; + } + + // get dsp preset gain values, update gain crossfaders, used when mixing dsp processed buffers into paintbuffer + SDEBUG_ShowAvgValue(); + + // the cache needs to hold the audio in memory during mixing, so tell it that mixing is starting + wavedatacache->OnMixBegin(); + + while ( g_paintedtime < endtime ) + { + VPROF("MIX_PaintChannels inner loop"); + // mix a full 'paintbuffer' of sound + + // clamp at paintbuffer size + end = endtime; + if (endtime - g_paintedtime > PAINTBUFFER_SIZE) + { + end = g_paintedtime + PAINTBUFFER_SIZE; + } + + // number of 44khz samples to mix into paintbuffer, up to paintbuffer size + count = end - g_paintedtime; + + // clear all mix buffers + g_AudioDevice->MixBegin( count ); + + // upsample all mix buffers. + // results in 44khz versions of: + // SOUND_BUFFER_ROOM, SOUND_BUFFER_FACING, SOUND_BUFFER_FACINGAWAY, SOUND_BUFFER_DRY, SOUND_BUFFER_SPEAKER, SOUND_BUFFER_SPECIALs + MIX_UpsampleAllPaintbuffers( list, end, count ); + + // apply appropriate dsp fx to each buffer, remix buffers into single quad output buffer + // apply 2 or 4ch filtering to IFACINGAWAY buffer + if ( g_bdirectionalfx ) + { + g_AudioDevice->ApplyDSPEffects( idsp_facingaway, MIX_GetPFrontFromIPaint(SOUND_BUFFER_FACINGAWAY), MIX_GetPRearFromIPaint(SOUND_BUFFER_FACINGAWAY), MIX_GetPCenterFromIPaint(SOUND_BUFFER_FACINGAWAY), count ); + } + + if ( !g_bDspOff && list.m_hasSpeakerChannels ) + { + // apply 1ch filtering to SOUND_BUFFER_SPEAKER + g_AudioDevice->ApplyDSPEffects( idsp_speaker, MIX_GetPFrontFromIPaint(SOUND_BUFFER_SPEAKER), MIX_GetPRearFromIPaint(SOUND_BUFFER_SPEAKER), MIX_GetPCenterFromIPaint(SOUND_BUFFER_SPEAKER), count ); + + // mix SOUND_BUFFER_SPEAKER with SOUND_BUFFER_ROOM and SOUND_BUFFER_FACING + MIX_ScalePaintBuffer( SOUND_BUFFER_SPEAKER, count, 0.7 ); + + MIX_MixPaintbuffers( SOUND_BUFFER_SPEAKER, SOUND_BUFFER_FACING, SOUND_BUFFER_FACING, count, 1.0 ); // +70% dry speaker + + MIX_ScalePaintBuffer( SOUND_BUFFER_SPEAKER, count, 0.43 ); + + MIX_MixPaintbuffers( SOUND_BUFFER_SPEAKER, SOUND_BUFFER_ROOM, SOUND_BUFFER_ROOM, count, 1.0 ); // +30% wet speaker + } + + if ( !g_bDspOff ) + { + // apply 1ch filtering to SOUND_BUFFER_SPECIALs + for ( int iDSP = 0; iDSP < list.m_nSpecialDSPs.Count(); ++iDSP ) + { + bool bFoundMixer = false; + + for ( int i = SOUND_BUFFER_SPECIAL_START; i < g_paintBuffers.Count(); ++i ) + { + paintbuffer_t *pSpecialBuffer = MIX_GetPPaintFromIPaint( i ); + if ( pSpecialBuffer->nSpecialDSP == list.m_nSpecialDSPs[ iDSP ] && pSpecialBuffer->idsp_specialdsp != -1 ) + { + g_AudioDevice->ApplyDSPEffects( pSpecialBuffer->idsp_specialdsp, MIX_GetPFrontFromIPaint( i ), MIX_GetPRearFromIPaint( i ), MIX_GetPCenterFromIPaint( i ), count ); + + // mix SOUND_BUFFER_SPECIALs with SOUND_BUFFER_ROOM and SOUND_BUFFER_FACING + MIX_ScalePaintBuffer( i, count, 0.7 ); + + MIX_MixPaintbuffers( i, SOUND_BUFFER_FACING, SOUND_BUFFER_FACING, count, 1.0 ); // +70% dry speaker + + MIX_ScalePaintBuffer( i, count, 0.43 ); + + MIX_MixPaintbuffers( i, SOUND_BUFFER_ROOM, SOUND_BUFFER_ROOM, count, 1.0 ); // +30% wet speaker + + bFoundMixer = true; + + break; + } + } + + // Couldn't find a mixer with the correct DSP, so make a new one! + if ( !bFoundMixer ) + { + bool bSurroundCenter = g_AudioDevice->IsSurroundCenter(); + bool bSurround = g_AudioDevice->IsSurround() || bSurroundCenter; + + int nIndex = g_paintBuffers.AddToTail(); + MIX_InitializePaintbuffer( &(g_paintBuffers[ nIndex ]), bSurround, bSurroundCenter ); + + g_paintBuffers[ nIndex ].flags = SOUND_BUSS_SPECIAL_DSP; + + // special dsp buffer mixes to mono + g_paintBuffers[ nIndex ].fsurround = false; + g_paintBuffers[ nIndex ].fsurround_center = false; + + g_paintBuffers[ nIndex ].idsp_specialdsp = -1; + g_paintBuffers[ nIndex ].nSpecialDSP = list.m_nSpecialDSPs[ iDSP ]; + + g_paintBuffers[ nIndex ].nPrevSpecialDSP = g_paintBuffers[ nIndex ].nSpecialDSP; + g_paintBuffers[ nIndex ].idsp_specialdsp = DSP_Alloc( g_paintBuffers[ nIndex ].nSpecialDSP, 300, 1 ); + } + } + } + + // apply dsp_room effects to room buffer + g_AudioDevice->ApplyDSPEffects( Get_idsp_room(), MIX_GetPFrontFromIPaint(SOUND_BUFFER_ROOM), MIX_GetPRearFromIPaint(SOUND_BUFFER_ROOM), MIX_GetPCenterFromIPaint(SOUND_BUFFER_ROOM), count ); + + // save room buffer surround status, in case we upconvert it + room_fsurround_sav = proom->fsurround; + room_fsurround_center_sav = proom->fsurround_center; + + // apply left/center/right/lrear/rrear spatial delays to room buffer + if ( b_spatial_delays && !g_bDspOff && !DSP_RoomDSPIsOff() ) + { + // upgrade mono room buffer to surround status so we can apply spatial delays to all channels + MIX_ConvertBufferToSurround( SOUND_BUFFER_ROOM ); + g_AudioDevice->ApplyDSPEffects( idsp_spatial, MIX_GetPFrontFromIPaint(SOUND_BUFFER_ROOM), MIX_GetPRearFromIPaint(SOUND_BUFFER_ROOM), MIX_GetPCenterFromIPaint(SOUND_BUFFER_ROOM), count ); + } + + if ( g_bdirectionalfx ) // KDB: perf + { + // Recombine IFACING and IFACINGAWAY buffers into SOUND_BUFFER_PAINT + MIX_MixPaintbuffers( SOUND_BUFFER_FACING, SOUND_BUFFER_FACINGAWAY, SOUND_BUFFER_PAINT, count, DSP_NOROOM_MIX ); + + // Add in dsp room fx to paintbuffer, mix at 75% + MIX_MixPaintbuffers( SOUND_BUFFER_ROOM, SOUND_BUFFER_PAINT, SOUND_BUFFER_PAINT, count, DSP_ROOM_MIX ); + } + else + { + // Mix IFACING buffer with SOUND_BUFFER_ROOM + // (SOUND_BUFFER_FACINGAWAY contains no data, IFACINGBBUFFER has full dry mix based on distance from listener) + // if dsp disabled, mix 100% facingbuffer, otherwise, mix 75% facingbuffer + roombuffer + float mix = g_bDspOff ? 1.0 : DSP_ROOM_MIX; + MIX_MixPaintbuffers( SOUND_BUFFER_ROOM, SOUND_BUFFER_FACING, SOUND_BUFFER_PAINT, count, mix ); + } + + // restore room buffer surround status, in case we upconverted it + proom->fsurround = room_fsurround_sav; + proom->fsurround_center = room_fsurround_center_sav; + + // Apply underwater fx dsp_water (serial in-line) + if ( bIsUnderwater ) + { + // BUG: if out of water, previous delays will be heard. must clear dly buffers. + g_AudioDevice->ApplyDSPEffects( idsp_water, MIX_GetPFrontFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPRearFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPCenterFromIPaint(SOUND_BUFFER_PAINT), count ); + } + + // find dsp gain + SDEBUG_GetAvgIn(SOUND_BUFFER_PAINT, count); + + // Apply player fx dsp_player (serial in-line) - does nothing if dsp fx are disabled + g_AudioDevice->ApplyDSPEffects( idsp_player, MIX_GetPFrontFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPRearFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPCenterFromIPaint(SOUND_BUFFER_PAINT), count ); + + // display dsp gain + SDEBUG_GetAvgOut(SOUND_BUFFER_PAINT, count); + +/* + // apply left/center/right/lrear/rrear spatial delays to paint buffer + + if ( b_spatial_delays ) + g_AudioDevice->ApplyDSPEffects( idsp_spatial, MIX_GetPFrontFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPRearFromIPaint(SOUND_BUFFER_PAINT), MIX_GetPCenterFromIPaint(SOUND_BUFFER_PAINT), count ); +*/ + // Add dry buffer, set output gain to water * player dsp gain (both 1.0 if not active) + + MIX_MixPaintbuffers( SOUND_BUFFER_PAINT, SOUND_BUFFER_DRY, SOUND_BUFFER_PAINT, count, 1.0); + + // clip all values > 16 bit down to 16 bit + // NOTE: This is required - the hardware buffer transfer routines no longer perform clipping. + MIX_CompressPaintbuffer( SOUND_BUFFER_PAINT, count ); + + // transfer SOUND_BUFFER_PAINT paintbuffer out to DMA buffer + MIX_SetCurrentPaintbuffer( SOUND_BUFFER_PAINT ); + + g_AudioDevice->TransferSamples( end ); + + g_paintedtime = end; + } + + // the cache needs to hold the audio in memory during mixing, so tell it that mixing is complete + wavedatacache->OnMixEnd(); +} + +// Applies volume scaling (evenly) to all fl,fr,rl,rr volumes +// used for voice ducking and panning between various mix busses +// Ensures if mixing to speaker buffer, only speaker sounds pass through + +// Called just before mixing wav data to current paintbuffer. +// a) if another player in a multiplayer game is speaking, scale all volumes down. +// b) if mixing to SOUND_BUFFER_ROOM, scale all volumes by ch.dspmix and dsp_room gain +// c) if mixing to SOUND_BUFFER_FACINGAWAY, scale all volumes by ch.dspface and dsp_facingaway gain +// d) If SURROUND_ON, but buffer is not surround, recombined front/rear volumes + +// returns false if channel is to be entirely skipped. + +bool MIX_ScaleChannelVolume( paintbuffer_t *ppaint, channel_t *pChannel, int volume[CCHANVOLUMES], int mixchans ) +{ + int i; + int mixflag = ppaint->flags; + float scale; + char wavtype = pChannel->wavtype; + float dspmix; + + // copy current channel volumes into output array + + ChannelCopyVolumes( pChannel, volume, 0, CCHANVOLUMES ); + + dspmix = pChannel->dspmix; + + // if dsp is off, or room dsp is off, mix 0% to mono room buffer, 100% to facing buffer + + if ( g_bDspOff || DSP_RoomDSPIsOff() ) + dspmix = 0.0; + + // duck all sound volumes except speaker's voice +#if !defined( NO_VOICE ) + int duckScale = min((int)(g_DuckScale * 256), g_SND_VoiceOverdriveInt); +#else + int duckScale = (int)(g_DuckScale * 256); +#endif + if( duckScale < 256 ) + { + if( pChannel->pMixer ) + { + CAudioSource *pSource = pChannel->pMixer->GetSource(); + if( !pSource->IsVoiceSource() ) + { + // Apply voice overdrive.. + for (i = 0; i < CCHANVOLUMES; i++) + volume[i] = (volume[i] * duckScale) >> 8; + } + } + } + + // If mixing to the room buss, adjust volume based on channel's dspmix setting. + // dspmix is DSP_MIX_MAX (~0.78) if sound is far from player, DSP_MIX_MIN (~0.24) if sound is near player + + if ( mixflag & SOUND_BUSS_ROOM ) + { + // set dsp mix volume, scaled by global dsp_volume + + float dspmixvol = fpmin(dspmix * g_dsp_volume, 1.0f); + + // if dspmix is 1.0, 100% of sound goes to SOUND_BUFFER_ROOM and 0% to SOUND_BUFFER_FACING + + for (i = 0; i < CCHANVOLUMES; i++) + volume[i] = (int)((float)(volume[i]) * dspmixvol); + } + + // If global dsp volume is less than 1, reduce dspmix (ie: increase dry volume) + // If gloabl dsp volume is greater than 1, do not reduce dspmix + + if (g_dsp_volume < 1.0) + dspmix *= g_dsp_volume; + + // If mixing to facing/facingaway buss, adjust volume based on sound entity's facing direction. + + // If sound directly faces player, ch->dspface = 1.0. If facing directly away, ch->dspface = -1.0. + // mix to lowpass buffer if facing away, to allpass if facing + + // scale 1.0 - facing player, scale 0, facing away + + scale = (pChannel->dspface + 1.0) / 2.0; + + // UNDONE: get front cone % from channel to set this. + + // bias scale such that 1.0 to 'cone' is considered facing. Facing cone narrows as cone -> 1.0 + // and 'cone' -> 0.0 becomes 1.0 -> 0.0 + + float cone = 0.6f; + + scale = scale * (1/cone); + + scale = clamp( scale, 0.0f, 1.0f ); + + // pan between facing and facing away buffers + + // if ( !g_bdirectionalfx || wavtype == CHAR_DOPPLER || wavtype == CHAR_OMNI || (wavtype == CHAR_DIRECTIONAL && mixchans == 2) ) + if ( !g_bdirectionalfx || wavtype != CHAR_DIRECTIONAL ) + { + // if no directional fx mix 0% to facingaway buffer + // if wavtype is DOPPLER, mix 0% to facingaway buffer - DOPPLER wavs have a custom mixer + // if wavtype is OMNI, mix 0% to facingaway buffer - OMNI wavs have no directionality + // if wavtype is DIRECTIONAL and stereo encoded, mix 0% to facingaway buffer - DIRECTIONAL STEREO wavs have a custom mixer + + scale = 1.0; + } + + if ( mixflag & SOUND_BUSS_FACING ) + { + // facing player + // if dspface is 1.0, 100% of sound goes to SOUND_BUFFER_FACING + + for (i = 0; i < CCHANVOLUMES; i++) + volume[i] = (int)((float)(volume[i]) * scale * (1.0 - dspmix)); + } + else if ( mixflag & SOUND_BUSS_FACINGAWAY ) + { + // facing away from player + // if dspface is 0.0, 100% of sound goes to SOUND_BUFFER_FACINGAWAY + + for (i = 0; i < CCHANVOLUMES; i++) + volume[i] = (int)((float)(volume[i]) * (1.0 - scale) * (1.0 - dspmix)); + } + + // NOTE: this must occur last in this routine: + + if ( g_AudioDevice->IsSurround() && !ppaint->fsurround ) + { + // if 4ch or 5ch spatialization on, but current mix buffer is 2ch, + // recombine front + rear volumes (revert to 2ch spatialization) + + volume[IFRONT_RIGHT] += volume[IREAR_RIGHT]; + volume[IFRONT_LEFT] += volume[IREAR_LEFT]; + + volume[IFRONT_RIGHTD] += volume[IREAR_RIGHTD]; + volume[IFRONT_LEFTD] += volume[IREAR_LEFTD]; + + // if 5 ch, recombine center channel vol + + if ( g_AudioDevice->IsSurroundCenter() ) + { + volume[IFRONT_RIGHT] += volume[IFRONT_CENTER] / 2; + volume[IFRONT_LEFT] += volume[IFRONT_CENTER] / 2; + + volume[IFRONT_RIGHTD] += volume[IFRONT_CENTERD] / 2; + volume[IFRONT_LEFTD] += volume[IFRONT_CENTERD] / 2; + } + + // clear rear & center volumes + + volume[IREAR_RIGHT] = 0; + volume[IREAR_LEFT] = 0; + volume[IFRONT_CENTER] = 0; + + volume[IREAR_RIGHTD] = 0; + volume[IREAR_LEFTD] = 0; + volume[IFRONT_CENTERD] = 0; + + } + + bool fzerovolume = true; + + for (i = 0; i < CCHANVOLUMES; i++) + { + volume[i] = clamp(volume[i], 0, 255); + + if (volume[i]) + fzerovolume = false; + } + + + if ( fzerovolume ) + { + // DevMsg ("Skipping mix of 0 volume sound! \n"); + return false; + } + + return true; +} + + +//=============================================================================== +// Low level mixing routines +//=============================================================================== +void Snd_WriteLinearBlastStereo16( void ) +{ +#if !id386 + int i; + int val; + + for ( i=0; i<snd_linear_count; i+=2 ) + { + // scale and clamp left 16bit signed: [0x8000, 0x7FFF] + val = ( snd_p[i] * snd_vol )>>8; + if ( val > 32767 ) + snd_out[i] = 32767; + else if ( val < -32768 ) + snd_out[i] = -32768; + else + snd_out[i] = val; + + // scale and clamp right 16bit signed: [0x8000, 0x7FFF] + val = ( snd_p[i+1] * snd_vol )>>8; + if ( val > 32767 ) + snd_out[i+1] = 32767; + else if ( val < -32768 ) + snd_out[i+1] = -32768; + else + snd_out[i+1] = val; + } +#else + __asm + { + // input data + mov ebx,snd_p + + // output data + mov edi,snd_out + + // iterate from end to beginning + mov ecx,snd_linear_count + + // scale table + mov esi,snd_vol + + // scale and clamp 16bit signed lsw: [0x8000, 0x7FFF] +WLBS16_LoopTop: + mov eax,[ebx+ecx*4-8] + imul eax,esi + sar eax,0x08 + cmp eax,0x7FFF + jg WLBS16_ClampHigh + cmp eax,0xFFFF8000 + jnl WLBS16_ClampDone + mov eax,0xFFFF8000 + jmp WLBS16_ClampDone +WLBS16_ClampHigh: + mov eax,0x7FFF +WLBS16_ClampDone: + + // scale and clamp 16bit signed msw: [0x8000, 0x7FFF] + mov edx,[ebx+ecx*4-4] + imul edx,esi + sar edx,0x08 + cmp edx,0x7FFF + jg WLBS16_ClampHigh2 + cmp edx,0xFFFF8000 + jnl WLBS16_ClampDone2 + mov edx,0xFFFF8000 + jmp WLBS16_ClampDone2 +WLBS16_ClampHigh2: + mov edx,0x7FFF +WLBS16_ClampDone2: + shl edx,0x10 + and eax,0xFFFF + or edx,eax + mov [edi+ecx*2-4],edx + + // two shorts per iteration + sub ecx,0x02 + jnz WLBS16_LoopTop + } +#endif +} + +void SND_InitScaletable (void) +{ + int i, j; + + for (i=0 ; i<SND_SCALE_LEVELS; i++) + for (j=0 ; j<256 ; j++) + snd_scaletable[i][j] = ((signed char)j) * i * (1<<SND_SCALE_SHIFT); +} + +void SND_PaintChannelFrom8(portable_samplepair_t *pOutput, int *volume, byte *pData8, int count) +{ +#if !id386 + int data; + int *lscale, *rscale; + int i; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + for (i=0 ; i<count ; i++) + { + data = pData8[i]; + + pOutput[i].left += lscale[data]; + pOutput[i].right += rscale[data]; + } +#else + // portable_samplepair_t structure +#define psp_left 0 +#define psp_right 4 +#define psp_size 8 + static int tempStore; + + __asm + { + // prologue + push ebp + + // esp = pOutput + mov eax, pOutput + mov tempStore, eax + xchg esp,tempStore + // ebx = volume + mov ebx,volume + // esi = pData8 + mov esi,pData8 + // ecx = count + mov ecx,count + + // These values depend on the setting of SND_SCALE_BITS + // The mask must mask off all the lower bits you aren't using in the multiply + // so for 7 bits, the mask is 0xFE, 6 bits 0xFC, etc. + // The shift must multiply by the table size. There are 256 4-byte values in the table at each level. + // So each index must be shifted left by 10, but since the bits we use are in the MSB rather than LSB + // they must be shifted right by 8 - SND_SCALE_BITS. e.g., for a 7 bit number the left shift is: + // 10 - (8-7) = 9. For a 5 bit number it's 10 - (8-5) = 7. + mov eax,[ebx] + mov edx,[ebx + 4] + and eax,0xFE + and edx,0xFE + + // shift up by 10 to index table, down by 1 to make the 7 MSB of the bytes an index + // eax = lscale + // edx = rscale + shl eax,0x09 + shl edx,0x09 + add eax,OFFSET snd_scaletable + add edx,OFFSET snd_scaletable + + // ebx = data byte + sub ebx,ebx + mov bl,[esi+ecx-1] + + // odd or even number of L/R samples + test ecx,0x01 + jz PCF8_Loop + + // process odd L/R sample + mov edi,[eax+ebx*4] + mov ebp,[edx+ebx*4] + add edi,[esp+ecx*psp_size-psp_size+psp_left] + add ebp,[esp+ecx*psp_size-psp_size+psp_right] + mov [esp+ecx*psp_size-psp_size+psp_left],edi + mov [esp+ecx*psp_size-psp_size+psp_right],ebp + mov bl,[esi+ecx-1-1] + + dec ecx + jz PCF8_Done + +PCF8_Loop: + // process L/R sample N + mov edi,[eax+ebx*4] + mov ebp,[edx+ebx*4] + add edi,[esp+ecx*psp_size-psp_size+psp_left] + add ebp,[esp+ecx*psp_size-psp_size+psp_right] + mov [esp+ecx*psp_size-psp_size+psp_left],edi + mov [esp+ecx*psp_size-psp_size+psp_right],ebp + mov bl,[esi+ecx-1-1] + + // process L/R sample N-1 + mov edi,[eax+ebx*4] + mov ebp,[edx+ebx*4] + add edi,[esp+ecx*psp_size-psp_size*2+psp_left] + add ebp,[esp+ecx*psp_size-psp_size*2+psp_right] + mov [esp+ecx*psp_size-psp_size*2+psp_left],edi + mov [esp+ecx*psp_size-psp_size*2+psp_right],ebp + mov bl,[esi+ecx-1-2] + + // two L/R samples per iteration + sub ecx,0x02 + jnz PCF8_Loop + +PCF8_Done: + // epilogue + xchg esp,tempStore + pop ebp + } +#endif +} + +//=============================================================================== +// SOFTWARE MIXING ROUTINES +//=============================================================================== + +// UNDONE: optimize these + +// grab samples from left source channel only and mix as if mono. +// volume array contains appropriate spatialization volumes for doppler left (incoming sound) +void SW_Mix8StereoDopplerLeft( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += lscale[pData[sampleIndex]]; + pOutput[i].right += rscale[pData[sampleIndex]]; + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + +// grab samples from right source channel only and mix as if mono. +// volume array contains appropriate spatialization volumes for doppler right (outgoing sound) +void SW_Mix8StereoDopplerRight( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += lscale[pData[sampleIndex+1]]; + pOutput[i].right += rscale[pData[sampleIndex+1]]; + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } + +} + + +// grab samples from left source channel only and mix as if mono. +// volume array contains appropriate spatialization volumes for doppler left (incoming sound) + +void SW_Mix16StereoDopplerLeft( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += (volume[0] * (int)(pData[sampleIndex]))>>8; + pOutput[i].right += (volume[1] * (int)(pData[sampleIndex]))>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + + +// grab samples from right source channel only and mix as if mono. +// volume array contains appropriate spatialization volumes for doppler right (outgoing sound) + +void SW_Mix16StereoDopplerRight( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += (volume[0] * (int)(pData[sampleIndex+1]))>>8; + pOutput[i].right += (volume[1] * (int)(pData[sampleIndex+1]))>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + +// mix left wav (front facing) with right wav (rear facing) based on soundfacing direction +void SW_Mix8StereoDirectional( float soundfacing, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int x; + int l,r; + signed char lb,rb; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + // if soundfacing -1.0, sound source is facing away from player + // if soundfacing 0.0, sound source is perpendicular to player + // if soundfacing 1.0, sound source is facing player + + int frontmix = (int)(256.0f * ((1.f + soundfacing) / 2.f)); // 0 -> 256 + + for ( int i = 0; i < outCount; i++ ) + { + lb = (pData[sampleIndex]); // get left byte + rb = (pData[sampleIndex+1]); // get right byte + + l = ((int)lb); + r = ((int)rb); + + x = ( r + ((( l - r ) * frontmix) >> 8) ); + + pOutput[i].left += lscale[x & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + + +// mix left wav (front facing) with right wav (rear facing) based on soundfacing direction +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. +void SW_Mix8StereoDirectional_Interp( float soundfacing, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interpl, interpr; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + int x; + + // if soundfacing -1.0, sound source is facing away from player + // if soundfacing 0.0, sound source is perpendicular to player + // if soundfacing 1.0, sound source is facing player + + int frontmix = (int)(256.0f * ((1.f + soundfacing) / 2.f)); // 0 -> 256 + + for ( int i = 0; i < outCount; i++ ) + { + // interpolate between first & second sample (the samples bordering sampleFrac12 fraction) + + first = (int)((signed char)(pData[sampleIndex])); // left byte + second = (int)((signed char)(pData[sampleIndex+2])); + + interpl = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + first = (int)((signed char)(pData[sampleIndex+1])); // right byte + second = (int)((signed char)(pData[sampleIndex+3])); + + interpr = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + // crossfade between right/left based on directional mix + + x = ( interpr + ((( interpl - interpr ) * frontmix) >> 8) ); + + pOutput[i].left += lscale[x & 0xFF]; // scale and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + + +// mix left wav (front facing) with right wav (rear facing) based on soundfacing direction + +void SW_Mix16StereoDirectional( float soundfacing, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + fixedint sampleIndex = 0; + fixedint sampleFrac = inputOffset; + + int x; + int l, r; + + // if soundfacing -1.0, sound source is facing away from player + // if soundfacing 0.0, sound source is perpendicular to player + // if soundfacing 1.0, sound source is facing player + + int frontmix = (int)(256.0f * ((1.f + soundfacing) / 2.f)); // 0 -> 256 + + for ( int i = 0; i < outCount; i++ ) + { + // get left, right samples + + l = (int)(pData[sampleIndex]); + r = (int)(pData[sampleIndex+1]); + + // crossfade between left & right based on front/rear facing + + x = ( r + ((( l - r ) * frontmix) >> 8) ); + + pOutput[i].left += (volume[0] * x) >> 8; + pOutput[i].right += (volume[1] * x) >> 8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + +// mix left wav (front facing) with right wav (rear facing) based on soundfacing direction +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. + +void SW_Mix16StereoDirectional_Interp( float soundfacing, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int x; + int first, second, interpl, interpr; + + // if soundfacing -1.0, sound source is facing away from player + // if soundfacing 0.0, sound source is perpendicular to player + // if soundfacing 1.0, sound source is facing player + + int frontmix = (int)(256.0f * ((1.f + soundfacing) / 2.f)); // 0 -> 256 + + for ( int i = 0; i < outCount; i++ ) + { + // get interpolated left, right samples + + first = (int)(pData[sampleIndex]); + second = (int)(pData[sampleIndex+2]); + + interpl = first + (((second - first) * (int)sampleFrac14) >> 14); + + first = (int)(pData[sampleIndex+1]); + second = (int)(pData[sampleIndex+3]); + + interpr = first + (((second - first) * (int)sampleFrac14) >> 14); + + // crossfade between left & right based on front/rear facing + + x = ( interpr + ((( interpl - interpr ) * frontmix) >> 8) ); + + pOutput[i].left += (volume[0] * x) >> 8; + pOutput[i].right += (volume[1] * x) >> 8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + + +// distance variant wav (left is close, right is far) +void SW_Mix8StereoDistVar( float distmix, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int x; + int l,r; + signed char lb, rb; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + // distmix 0 - sound is near player (100% wav left) + // distmix 1.0 - sound is far from player (100% wav right) + + int nearmix = (int)(256.0f * (1.0f - distmix)); + int farmix = (int)(256.0f * distmix); + + // if mixing at max or min range, skip crossfade (KDB: perf) + + if (!nearmix) + { + for ( int i = 0; i < outCount; i++ ) + { + rb = (pData[sampleIndex+1]); // get right byte + x = (int) rb; + + pOutput[i].left += lscale[x & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } + return; + } + + if (!farmix) + { + for ( int i = 0; i < outCount; i++ ) + { + + lb = (pData[sampleIndex]); // get left byte + x = (int) lb; + + pOutput[i].left += lscale[x & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } + return; + } + + // crossfade left/right + + for ( int i = 0; i < outCount; i++ ) + { + + lb = (pData[sampleIndex]); // get left byte + rb = (pData[sampleIndex+1]); // get right byte + + l = (int)lb; + r = (int)rb; + + x = ( l + (((r - l) * farmix ) >> 8) ); + + pOutput[i].left += lscale[x & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + + +// distance variant wav (left is close, right is far) +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. +void SW_Mix8StereoDistVar_Interp( float distmix, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int x; + + // distmix 0 - sound is near player (100% wav left) + // distmix 1.0 - sound is far from player (100% wav right) + + int nearmix = (int)(256.0f * (1.0f - distmix)); + int farmix = (int)(256.0f * distmix); + + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interpl, interpr; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + // if mixing at max or min range, skip crossfade (KDB: perf) + + if (!nearmix) + { + for ( int i = 0; i < outCount; i++ ) + { + first = (int)((signed char)(pData[sampleIndex+1])); // right sample + second = (int)((signed char)(pData[sampleIndex+3])); + + interpr = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + pOutput[i].left += lscale[interpr & 0xFF]; // scale and convert to 16 bit + pOutput[i].right += rscale[interpr & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + + } + return; + } + + if (!farmix) + { + for ( int i = 0; i < outCount; i++ ) + { + first = (int)((signed char)(pData[sampleIndex])); // left sample + second = (int)((signed char)(pData[sampleIndex+2])); + + interpl = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + pOutput[i].left += lscale[interpl & 0xFF]; // scale and convert to 16 bit + pOutput[i].right += rscale[interpl & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } + return; + } + + // crossfade left/right + + for ( int i = 0; i < outCount; i++ ) + { + // interpolate between first & second sample (the samples bordering sampleFrac14 fraction) + + first = (int)((signed char)(pData[sampleIndex])); + second = (int)((signed char)(pData[sampleIndex+2])); + + interpl = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + first = (int)((signed char)(pData[sampleIndex+1])); + second = (int)((signed char)(pData[sampleIndex+3])); + + interpr = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + // crossfade between left and right based on distance mix + + x = ( interpl + (((interpr - interpl) * farmix ) >> 8) ); + + pOutput[i].left += lscale[x & 0xFF]; // scale and convert to 16 bit + pOutput[i].right += rscale[x & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + + +// distance variant wav (left is close, right is far) + +void SW_Mix16StereoDistVar( float distmix, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int x; + int l,r; + + // distmix 0 - sound is near player (100% wav left) + // distmix 1.0 - sound is far from player (100% wav right) + + int nearmix = Float2Int(256.0f * (1.f - distmix)); + int farmix = Float2Int(256.0f * distmix); + + // if mixing at max or min range, skip crossfade (KDB: perf) + + if (!nearmix) + { + for ( int i = 0; i < outCount; i++ ) + { + x = pData[sampleIndex+1]; // right sample + + pOutput[i].left += (volume[0] * x)>>8; + pOutput[i].right += (volume[1] * x)>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } + return; + } + + if (!farmix) + { + for ( int i = 0; i < outCount; i++ ) + { + x = pData[sampleIndex]; // left sample + + pOutput[i].left += (volume[0] * x)>>8; + pOutput[i].right += (volume[1] * x)>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } + return; + } + + // crossfade left/right + + for ( int i = 0; i < outCount; i++ ) + { + l = pData[sampleIndex]; + r = pData[sampleIndex+1]; + + x = ( l + (((r - l) * farmix) >> 8) ); + + pOutput[i].left += (volume[0] * x)>>8; + pOutput[i].right += (volume[1] * x)>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + +// distance variant wav (left is close, right is far) +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. + +void SW_Mix16StereoDistVar_Interp( float distmix, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int x; + + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interpl, interpr; + + + // distmix 0 - sound is near player (100% wav left) + // distmix 1.0 - sound is far from player (100% wav right) + + int nearmix = Float2Int(256.0f * (1.f - distmix)); + int farmix = Float2Int(256.0f * distmix); + + // if mixing at max or min range, skip crossfade (KDB: perf) + + if (!nearmix) + { + for ( int i = 0; i < outCount; i++ ) + { + first = (int)(pData[sampleIndex+1]); // right sample + second = (int)(pData[sampleIndex+3]); + interpr = first + (((second - first) * (int)sampleFrac14) >> 14); + + pOutput[i].left += (volume[0] * interpr)>>8; + pOutput[i].right += (volume[1] * interpr)>>8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } + return; + } + + if (!farmix) + { + for ( int i = 0; i < outCount; i++ ) + { + first = (int)(pData[sampleIndex]); // left sample + second = (int)(pData[sampleIndex+2]); + interpl = first + (((second - first) * (int)sampleFrac14) >> 14); + + pOutput[i].left += (volume[0] * interpl)>>8; + pOutput[i].right += (volume[1] * interpl)>>8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } + return; + } + + // crossfade left/right + + for ( int i = 0; i < outCount; i++ ) + { + first = (int)(pData[sampleIndex]); + second = (int)(pData[sampleIndex+2]); + interpl = first + (((second - first) * (int)sampleFrac14) >> 14); + + first = (int)(pData[sampleIndex+1]); + second = (int)(pData[sampleIndex+3]); + interpr = first + (((second - first) * (int)sampleFrac14) >> 14); + + // crossfade between left & right samples + + x = ( interpl + (((interpr - interpl) * farmix) >> 8) ); + + pOutput[i].left += (volume[0] * x) >> 8; + pOutput[i].right += (volume[1] * x) >> 8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + +void SW_Mix8Mono( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + // Not using pitch shift? + if ( rateScaleFix == FIX(1) ) + { + // native code + SND_PaintChannelFrom8( pOutput, volume, (byte *)pData, outCount ); + return; + } + + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += lscale[pData[sampleIndex]]; + pOutput[i].right += rscale[pData[sampleIndex]]; + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac); + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + + +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. +void SW_Mix8Mono_Interp( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interp; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + // iterate 0th sample to outCount-1 sample + + for (int i = 0; i < outCount; i++ ) + { + // interpolate between first & second sample (the samples bordering sampleFrac12 fraction) + + first = (int)((signed char)(pData[sampleIndex])); + second = (int)((signed char)(pData[sampleIndex+1])); + + interp = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + pOutput[i].left += lscale[interp & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[interp & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14); + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + +void SW_Mix8Stereo( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += lscale[pData[sampleIndex]]; + pOutput[i].right += rscale[pData[sampleIndex+1]]; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + + +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. +void SW_Mix8Stereo_Interp( portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interpl, interpr; + int *lscale, *rscale; + + lscale = snd_scaletable[volume[0] >> SND_SCALE_SHIFT]; + rscale = snd_scaletable[volume[1] >> SND_SCALE_SHIFT]; + + // iterate 0th sample to outCount-1 sample + + for (int i = 0; i < outCount; i++ ) + { + // interpolate between first & second sample (the samples bordering sampleFrac12 fraction) + + first = (int)((signed char)(pData[sampleIndex])); // left + second = (int)((signed char)(pData[sampleIndex+2])); + + interpl = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + first = (int)((signed char)(pData[sampleIndex+1])); // right + second = (int)((signed char)(pData[sampleIndex+3])); + + interpr = first + ( ((second - first) * (int)sampleFrac14) >> 14 ); + + pOutput[i].left += lscale[interpl & 0xFF]; // multiply by volume and convert to 16 bit + pOutput[i].right += rscale[interpr & 0xFF]; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + +void SW_Mix16Mono_Shift( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int vol0 = volume[0]; + int vol1 = volume[1]; + +#if !id386 + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += (vol0 * (int)(pData[sampleIndex]))>>8; + pOutput[i].right += (vol1 * (int)(pData[sampleIndex]))>>8; + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac); + sampleFrac = FIX_FRACPART(sampleFrac); + } +#else + // in assembly, you can make this 32.32 instead of 4.28 and use the carry flag instead of masking + int rateScaleInt = FIX_INTPART(rateScaleFix); + unsigned int rateScaleFrac = FIX_FRACPART(rateScaleFix) << (32-FIX_BITS); + + __asm + { + mov eax, volume ; + movq mm0, DWORD PTR [eax] ; vol1, vol0 (32-bits each) + packssdw mm0, mm0 ; pack and replicate... vol1, vol0, vol1, vol0 (16-bits each) + //pxor mm7, mm7 ; mm7 is my zero register... + + xor esi, esi + mov eax, DWORD PTR [pOutput] ; store initial output ptr + mov edx, DWORD PTR [pData] ; store initial input ptr + mov ebx, inputOffset; + mov ecx, outCount; + +BEGINLOAD: + movd mm2, WORD PTR [edx+2*esi] ; load first piece of data from pData + punpcklwd mm2, mm2 ; 0, 0, pData_1st, pData_1st + + add ebx, rateScaleFrac ; do the crazy fixed integer math + adc esi, rateScaleInt + + movd mm3, WORD PTR [edx+2*esi] ; load second piece of data from pData + punpcklwd mm3, mm3 ; 0, 0, pData_2nd, pData_2nd + punpckldq mm2, mm3 ; pData_2nd, pData_2nd, pData_2nd, pData_2nd + + add ebx, rateScaleFrac ; do the crazy fixed integer math + adc esi, rateScaleInt + + movq mm3, mm2 ; copy the goods + pmullw mm2, mm0 ; pData_2nd*vol1, pData_2nd*vol0, pData_1st*vol1, pData_1st*vol0 (bits 0-15) + pmulhw mm3, mm0 ; pData_2nd*vol1, pData_2nd*vol0, pData_1st*vol1, pData_1st*vol0 (bits 16-31) + + movq mm4, mm2 ; copy + movq mm5, mm3 ; copy + + punpcklwd mm2, mm3 ; pData_1st*vol1, pData_1st*vol0 (bits 0-31) + punpckhwd mm4, mm5 ; pData_2nd*vol1, pData_2nd*vol0 (bits 0-31) + psrad mm2, 8 ; shift right by 8 + psrad mm4, 8 ; shift right by 8 + + add ecx, -2 ; decrement i-value + paddd mm2, QWORD PTR [eax] ; add to existing vals + paddd mm4, QWORD PTR [eax+8] ; + + movq QWORD PTR [eax], mm2 ; store back + movq QWORD PTR [eax+8], mm4 ; + + add eax, 10h ; + cmp ecx, 01h ; see if we can quit + jg BEGINLOAD ; Kipp Owens is a doof... + jl END ; Nick Shaffner is killing me... + + movsx edi, WORD PTR [edx+2*esi] ; load first 16 bit val and zero-extend + imul edi, vol0 ; multiply pData[sampleIndex] by volume[0] + sar edi, 08h ; divide by 256 + add DWORD PTR [eax], edi ; add to pOutput[i].left + + movsx edi, WORD PTR [edx+2*esi] ; load same 16 bit val and zero-extend (cuz I thrashed the reg) + imul edi, vol1 ; multiply pData[sampleIndex] by volume[1] + sar edi, 08h ; divide by 256 + add DWORD PTR [eax+04h], edi ; add to pOutput[i].right +END: + emms; + } +#endif +} + +void SW_Mix16Mono_NoShift( portable_samplepair_t *pOutput, int *volume, short *pData, int outCount ) +{ + int vol0 = volume[0]; + int vol1 = volume[1]; +#if !id386 + for ( int i = 0; i < outCount; i++ ) + { + int x = *pData++; + pOutput[i].left += (x * vol0) >> 8; + pOutput[i].right += (x * vol1) >> 8; + } +#else + __asm + { + mov eax, volume ; + movq mm0, DWORD PTR [eax] ; vol1, vol0 (32-bits each) + packssdw mm0, mm0 ; pack and replicate... vol1, vol0, vol1, vol0 (16-bits each) + //pxor mm7, mm7 ; mm7 is my zero register... + + mov eax, DWORD PTR [pOutput] ; store initial output ptr + mov edx, DWORD PTR [pData] ; store initial input ptr + mov ecx, outCount; + +BEGINLOAD: + movd mm2, WORD PTR [edx] ; load first piece o data from pData + punpcklwd mm2, mm2 ; 0, 0, pData_1st, pData_1st + add edx,2 ; move to the next sample + + movd mm3, WORD PTR [edx] ; load second piece o data from pData + punpcklwd mm3, mm3 ; 0, 0, pData_2nd, pData_2nd + punpckldq mm2, mm3 ; pData_2nd, pData_2nd, pData_2nd, pData_2nd + + add edx,2 ; move to the next sample + + movq mm3, mm2 ; copy the goods + pmullw mm2, mm0 ; pData_2nd*vol1, pData_2nd*vol0, pData_1st*vol1, pData_1st*vol0 (bits 0-15) + pmulhw mm3, mm0 ; pData_2nd*vol1, pData_2nd*vol0, pData_1st*vol1, pData_1st*vol0 (bits 16-31) + + movq mm4, mm2 ; copy + movq mm5, mm3 ; copy + + punpcklwd mm2, mm3 ; pData_1st*vol1, pData_1st*vol0 (bits 0-31) + punpckhwd mm4, mm5 ; pData_2nd*vol1, pData_2nd*vol0 (bits 0-31) + psrad mm2, 8 ; shift right by 8 + psrad mm4, 8 ; shift right by 8 + + add ecx, -2 ; decrement i-value + paddd mm2, QWORD PTR [eax] ; add to existing vals + paddd mm4, QWORD PTR [eax+8] ; + + movq QWORD PTR [eax], mm2 ; store back + movq QWORD PTR [eax+8], mm4 ; + + add eax, 10h ; + cmp ecx, 01h ; see if we can quit + jg BEGINLOAD ; I can cut and paste code! + jl END ; + + movsx edi, WORD PTR [edx] ; load first 16 bit val and zero-extend + mov esi,edi ; save a copy for the other channel + imul edi, vol0 ; multiply pData[sampleIndex] by volume[0] + sar edi, 08h ; divide by 256 + add DWORD PTR [eax], edi ; add to pOutput[i].left + + ; esi has a copy, use it now + imul esi, vol1 ; multiply pData[sampleIndex] by volume[1] + sar esi, 08h ; divide by 256 + add DWORD PTR [eax+04h], esi ; add to pOutput[i].right +END: + emms; + } +#endif +} + +void SW_Mix16Mono( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + if ( rateScaleFix == FIX(1) ) + { + SW_Mix16Mono_NoShift( pOutput, volume, pData, outCount ); + } + else + { + SW_Mix16Mono_Shift( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + } +} + +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. + +void SW_Mix16Mono_Interp( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interp; + + for ( int i = 0; i < outCount; i++ ) + { + first = (int)(pData[sampleIndex]); + second = (int)(pData[sampleIndex+1]); + + interp = first + (((second - first) * (int)sampleFrac14) >> 14); + + pOutput[i].left += (volume[0] * interp) >> 8; + pOutput[i].right += (volume[1] * interp) >> 8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14); + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} + +void SW_Mix16Stereo( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + int sampleIndex = 0; + fixedint sampleFrac = inputOffset; + + for ( int i = 0; i < outCount; i++ ) + { + pOutput[i].left += (volume[0] * (int)(pData[sampleIndex]))>>8; + pOutput[i].right += (volume[1] * (int)(pData[sampleIndex+1]))>>8; + + sampleFrac += rateScaleFix; + sampleIndex += FIX_INTPART(sampleFrac)<<1; + sampleFrac = FIX_FRACPART(sampleFrac); + } +} + +// interpolating pitch shifter - sample(s) from preceding buffer are preloaded in +// pData buffer, ensuring we can always provide 'outCount' samples. + +void SW_Mix16Stereo_Interp( portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + fixedint sampleIndex = 0; + fixedint rateScaleFix14 = FIX_28TO14(rateScaleFix); // convert 28 bit fixed point to 14 bit fixed point + fixedint sampleFrac14 = FIX_28TO14(inputOffset); + + int first, second, interpl, interpr; + + for ( int i = 0; i < outCount; i++ ) + { + first = (int)(pData[sampleIndex]); + second = (int)(pData[sampleIndex+2]); + + interpl = first + (((second - first) * (int)sampleFrac14) >> 14); + + first = (int)(pData[sampleIndex+1]); + second = (int)(pData[sampleIndex+3]); + + interpr = first + (((second - first) * (int)sampleFrac14) >> 14); + + pOutput[i].left += (volume[0] * interpl) >> 8; + pOutput[i].right += (volume[1] * interpr) >> 8; + + sampleFrac14 += rateScaleFix14; + sampleIndex += FIX_INTPART14(sampleFrac14)<<1; + sampleFrac14 = FIX_FRACPART14(sampleFrac14); + } +} +// return true if mixer should use high quality pitch interpolation for this sound + +bool FUseHighQualityPitch( channel_t *pChannel ) +{ + // do not use interpolating pitch shifter if: + // low quality flag set on sound (ie: wave name is prepended with CHAR_FAST_PITCH) + // or pitch has no fractional part + // or snd_pitchquality is 0 + if ( !snd_pitchquality.GetInt() || pChannel->flags.bfast_pitch ) + return false; + + return ( (pChannel->pitch != floor(pChannel->pitch)) ); +} + +//=============================================================================== +// DISPATCHERS FOR MIXING ROUTINES +//=============================================================================== +void Mix8MonoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix8Mono_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix8Mono( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); +} + +void Mix16MonoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix16Mono_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + // fast native coded mixers with lower quality pitch shift + SW_Mix16Mono( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); +} + +void Mix8StereoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + switch ( pChannel->wavtype ) + { + case CHAR_DOPPLER: + SW_Mix8StereoDopplerLeft( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + SW_Mix8StereoDopplerRight( pOutput, &volume[IFRONT_LEFTD], pData, inputOffset, rateScaleFix, outCount ); + break; + + case CHAR_DIRECTIONAL: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix8StereoDirectional_Interp( pChannel->dspface, pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix8StereoDirectional( pChannel->dspface, pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + + case CHAR_DISTVARIANT: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix8StereoDistVar_Interp( pChannel->distmix, pOutput, volume, pData, inputOffset, rateScaleFix, outCount); + else + SW_Mix8StereoDistVar( pChannel->distmix, pOutput, volume, pData, inputOffset, rateScaleFix, outCount); + break; + + case CHAR_OMNI: + // non directional stereo - all channel volumes are the same + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix8Stereo_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix8Stereo( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + + default: + case CHAR_SPATIALSTEREO: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix8Stereo_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix8Stereo( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + } +} + + +void Mix16StereoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ) +{ + switch ( pChannel->wavtype ) + { + case CHAR_DOPPLER: + SW_Mix16StereoDopplerLeft( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + SW_Mix16StereoDopplerRight( pOutput, &volume[IFRONT_LEFTD], pData, inputOffset, rateScaleFix, outCount ); + break; + + case CHAR_DIRECTIONAL: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix16StereoDirectional_Interp( pChannel->dspface, pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix16StereoDirectional( pChannel->dspface, pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + + case CHAR_DISTVARIANT: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix16StereoDistVar_Interp( pChannel->distmix, pOutput, volume, pData, inputOffset, rateScaleFix, outCount); + else + SW_Mix16StereoDistVar( pChannel->distmix, pOutput, volume, pData, inputOffset, rateScaleFix, outCount); + break; + + case CHAR_OMNI: + // non directional stereo - all channel volumes are same + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix16Stereo_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix16Stereo( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + + default: + case CHAR_SPATIALSTEREO: + if ( FUseHighQualityPitch( pChannel ) ) + SW_Mix16Stereo_Interp( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + else + SW_Mix16Stereo( pOutput, volume, pData, inputOffset, rateScaleFix, outCount ); + break; + } +} + + +//=============================================================================== +// Client entity mouth movement code. Set entity mouthopen variable, based +// on the sound envelope of the voice channel playing. +// KellyB 10/22/97 +//=============================================================================== + + +extern IBaseClientDLL *g_ClientDLL; + +// called when voice channel is first opened on this entity +static CMouthInfo *GetMouthInfoForChannel( channel_t *pChannel ) +{ +#ifndef DEDICATED + // If it's a sound inside the client UI, ask the client for the mouthinfo + if ( pChannel->soundsource == SOUND_FROM_UI_PANEL ) + return g_ClientDLL ? g_ClientDLL->GetClientUIMouthInfo() : NULL; +#endif + + int mouthentity = pChannel->speakerentity == -1 ? pChannel->soundsource : pChannel->speakerentity; + + IClientEntity *pClientEntity = entitylist->GetClientEntity( mouthentity ); + + if( !pClientEntity ) + return NULL; + + return pClientEntity->GetMouth(); +} + +void SND_InitMouth( channel_t *pChannel ) +{ + if ( SND_IsMouth( pChannel ) ) + { + CMouthInfo *pMouth = GetMouthInfoForChannel(pChannel); + // init mouth movement vars + if ( pMouth ) + { + pMouth->mouthopen = 0; + pMouth->sndavg = 0; + pMouth->sndcount = 0; + if ( pChannel->sfx->pSource && pChannel->sfx->pSource->GetSentence() ) + { + pMouth->AddSource( pChannel->sfx->pSource, pChannel->flags.m_bIgnorePhonemes ); + } + } + } +} + +// called when channel stops + +void SND_CloseMouth(channel_t *pChannel) +{ + if ( SND_IsMouth( pChannel ) ) + { + CMouthInfo *pMouth = GetMouthInfoForChannel(pChannel); + if ( pMouth ) + { + // shut mouth + int idx = pMouth->GetIndexForSource( pChannel->sfx->pSource ); + + if ( idx != UNKNOWN_VOICE_SOURCE ) + { + pMouth->RemoveSourceByIndex(idx); + } + else + { + pMouth->ClearVoiceSources(); + } + pMouth->mouthopen = 0; + } + } +} + +#define CAVGSAMPLES 10 +// need this to make the debug code below work. +//#include "snd_wave_source.h" +void SND_MoveMouth8( channel_t *ch, CAudioSource *pSource, int count ) +{ + int data; + char *pdata = NULL; + int i; + int savg; + int scount; + + CMouthInfo *pMouth = GetMouthInfoForChannel( ch ); + + if ( !pMouth ) + return; + + if ( pSource->GetSentence() ) + { + int idx = pMouth->GetIndexForSource( pSource ); + + if ( idx == UNKNOWN_VOICE_SOURCE ) + { + if ( pMouth->AddSource( pSource, ch->flags.m_bIgnorePhonemes ) == NULL ) + { + DevMsg( 1, "out of voice sources, won't lipsync %s\n", ch->sfx->getname() ); +#if 0 + for ( int i = 0; i < pMouth->GetNumVoiceSources(); i++ ) + { + CVoiceData *pVoice = pMouth->GetVoiceSource(i); + CAudioSourceWave *pWave = dynamic_cast<CAudioSourceWave *>(pVoice->GetSource()); + const char *pName = "unknown"; + if ( pWave && pWave->GetName() ) + pName = pWave->GetName(); + Msg("Playing %s...\n", pName ); + } +#endif + } + } + else + { + // Update elapsed time from mixer + CVoiceData *vd = pMouth->GetVoiceSource( idx ); + Assert( vd ); + if ( vd ) + { + Assert( pSource->SampleRate() > 0 ); + + float elapsed = ( float )ch->pMixer->GetSamplePosition() / ( float )pSource->SampleRate(); + + vd->SetElapsedTime( elapsed ); + } + } + } + + if ( IsX360() ) + { + // not supporting because data is assumed to be 8 bit and bypasses mixer (decoding) + return; + } + + if ( pMouth->NeedsEnvelope() ) + { + int availableSamples = pSource->GetOutputData((void**)&pdata, ch->pMixer->GetSamplePosition(), count, NULL ); + + if( pdata == NULL ) + return; + + i = 0; + scount = pMouth->sndcount; + savg = 0; + + while ( i < availableSamples && scount < CAVGSAMPLES ) + { + data = pdata[i]; + savg += abs(data); + + i += 80 + ((byte)data & 0x1F); + scount++; + } + + pMouth->sndavg += savg; + pMouth->sndcount = (byte) scount; + + if ( pMouth->sndcount >= CAVGSAMPLES ) + { + pMouth->mouthopen = pMouth->sndavg / CAVGSAMPLES; + pMouth->sndavg = 0; + pMouth->sndcount = 0; + } + } + else + { + pMouth->mouthopen = 0; + } +} + + +void SND_UpdateMouth( channel_t *pChannel ) +{ + CMouthInfo *m = GetMouthInfoForChannel( pChannel ); + if ( !m ) + return; + + if ( pChannel->sfx ) + { + m->AddSource( pChannel->sfx->pSource, pChannel->flags.m_bIgnorePhonemes ); + } +} + + +void SND_ClearMouth( channel_t *pChannel ) +{ + CMouthInfo *m = GetMouthInfoForChannel( pChannel ); + if ( !m ) + return; + + if ( pChannel->sfx ) + { + m->RemoveSource( pChannel->sfx->pSource ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pChannel - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool SND_IsMouth( channel_t *pChannel ) +{ +#ifndef DEDICATED + if ( pChannel->soundsource == SOUND_FROM_UI_PANEL ) + return true; +#endif + + if ( !entitylist ) + { + return false; + } + + if ( pChannel->entchannel == CHAN_VOICE || pChannel->entchannel == CHAN_VOICE2 ) + { + return true; + } + + if ( pChannel->sfx && + pChannel->sfx->pSource && + pChannel->sfx->pSource->GetSentence() ) + { + return true; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pChannel - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool SND_ShouldPause( channel_t *pChannel ) +{ + return pChannel->flags.m_bShouldPause; +} + +//=============================================================================== +// Movie recording support +//=============================================================================== + +void SND_RecordInit() +{ + g_paintedtime = 0; + g_soundtime = 0; + + // TMP Wave file supports stereo only, so force stereo + if ( snd_surround.GetInt() != 2 ) + { + snd_surround.SetValue( 2 ); + } +} + +void SND_MovieStart( void ) +{ + if ( IsX360() ) + return; + + if ( !cl_movieinfo.IsRecording() ) + return; + + SND_RecordInit(); + + // 44k: engine playback rate is now 44100...changed from 22050 + if ( cl_movieinfo.DoWav() ) + { + WaveCreateTmpFile( cl_movieinfo.moviename, SOUND_DMA_SPEED, 16, 2 ); + } +} + +void SND_MovieEnd( void ) +{ + if ( IsX360() ) + return; + + if ( !cl_movieinfo.IsRecording() ) + { + return; + } + + if ( cl_movieinfo.DoWav() ) + { + WaveFixupTmpFile( cl_movieinfo.moviename ); + } +} + +bool SND_IsRecording() +{ + return ( ( IsReplayRendering() || cl_movieinfo.IsRecording() ) && !Con_IsVisible() ); +} + + + +extern IVideoRecorder *g_pVideoRecorder; +void SND_RecordBuffer( void ) +{ + if ( IsX360() ) + return; + + if ( !SND_IsRecording() ) + return; + + int i; + int val; + int bufferSize = snd_linear_count * sizeof(short); + short *tmp = (short *)_alloca( bufferSize ); + + for (i=0 ; i<snd_linear_count ; i+=2) + { + val = (snd_p[i]*snd_vol)>>8; + tmp[i] = CLIP(val); + + val = (snd_p[i+1]*snd_vol)>>8; + tmp[i+1] = CLIP(val); + } + + if ( IsReplayRendering() ) + { +#if defined( REPLAY_ENABLED ) + extern IClientReplayContext *g_pClientReplayContext; + IReplayMovieRenderer *pMovieRenderer = g_pClientReplayContext->GetMovieRenderer(); + if ( IsReplayRendering() && pMovieRenderer && pMovieRenderer->IsAudioSyncFrame() ) + { + pMovieRenderer->RenderAudio( (unsigned char *)tmp, bufferSize, snd_linear_count ); + } +#endif + } + else + { + if ( cl_movieinfo.DoWav() ) + { + WaveAppendTmpFile( cl_movieinfo.moviename, tmp, 16, snd_linear_count ); + } + + if ( cl_movieinfo.DoVideoSound() ) + { + g_pVideoRecorder->AppendAudioSamples( tmp, bufferSize ); + } + } +} diff --git a/engine/audio/private/snd_mix_buf.h b/engine/audio/private/snd_mix_buf.h new file mode 100644 index 0000000..b51da6d --- /dev/null +++ b/engine/audio/private/snd_mix_buf.h @@ -0,0 +1,107 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_MIX_BUF_H +#define SND_MIX_BUF_H + +#if defined( _WIN32 ) +#pragma once +#endif + +// OPTIMIZE: note that making this larger will not increase performance (12/27/03) +#define PAINTBUFFER_SIZE 1020 // 44k: was 512 + +#define PAINTBUFFER (g_curpaintbuffer) +#define REARPAINTBUFFER (g_currearpaintbuffer) +#define CENTERPAINTBUFFER (g_curcenterpaintbuffer) + +enum SoundBufferType_t +{ + SOUND_BUFFER_PAINT = 0, + SOUND_BUFFER_ROOM, + SOUND_BUFFER_FACING, + SOUND_BUFFER_FACINGAWAY, + SOUND_BUFFER_DRY, + SOUND_BUFFER_SPEAKER, + + SOUND_BUFFER_BASETOTAL, + SOUND_BUFFER_SPECIAL_START = SOUND_BUFFER_BASETOTAL +}; + +// !!! if this is changed, it much be changed in native assembly too !!! +struct portable_samplepair_t +{ + int left; + int right; +}; + +// sound mixing buffer +#define CPAINTFILTERMEM 3 +#define CPAINTFILTERS 4 // maximum number of consecutive upsample passes per paintbuffer + +struct paintbuffer_t +{ + bool factive; // if true, mix to this paintbuffer using flags + bool fsurround; // if true, mix to front and rear paintbuffers using flags + bool fsurround_center; // if true, mix to front, rear and center paintbuffers using flags + + int idsp_specialdsp; + int nPrevSpecialDSP; + int nSpecialDSP; + + int flags; // SOUND_BUSS_ROOM, SOUND_BUSS_FACING, SOUND_BUSS_FACINGAWAY, SOUND_BUSS_SPEAKER, SOUND_BUSS_SPECIAL_DSP, SOUND_BUSS_DRY + + portable_samplepair_t *pbuf; // front stereo mix buffer, for 2 or 4 channel mixing + portable_samplepair_t *pbufrear; // rear mix buffer, for 4 channel mixing + portable_samplepair_t *pbufcenter; // center mix buffer, for 5 channel mixing + + int ifilter; // current filter memory buffer to use for upsampling pass + + portable_samplepair_t fltmem[CPAINTFILTERS][CPAINTFILTERMEM]; // filter memory, for upsampling with linear or cubic interpolation + portable_samplepair_t fltmemrear[CPAINTFILTERS][CPAINTFILTERMEM]; // filter memory, for upsampling with linear or cubic interpolation + portable_samplepair_t fltmemcenter[CPAINTFILTERS][CPAINTFILTERMEM]; // filter memory, for upsampling with linear or cubic interpolation +}; + +extern "C" +{ + +extern portable_samplepair_t *g_paintbuffer; + +// temp paintbuffer - not included in main list of paintbuffers +extern portable_samplepair_t *g_temppaintbuffer; + +extern CUtlVector< paintbuffer_t > g_paintBuffers; + +extern void MIX_SetCurrentPaintbuffer( int ipaintbuffer ); +extern int MIX_GetCurrentPaintbufferIndex( void ); +extern paintbuffer_t *MIX_GetCurrentPaintbufferPtr( void ); +extern paintbuffer_t *MIX_GetPPaintFromIPaint( int ipaintbuffer ); +extern void MIX_ClearAllPaintBuffers( int SampleCount, bool clearFilters ); +extern bool MIX_InitAllPaintbuffers(void); +extern void MIX_FreeAllPaintbuffers(void); + +extern portable_samplepair_t *g_curpaintbuffer; +extern portable_samplepair_t *g_currearpaintbuffer; +extern portable_samplepair_t *g_curcenterpaintbuffer; + +}; + +// must be at least PAINTBUFFER_SIZE+1 for upsampling +#define PAINTBUFFER_MEM_SIZE (PAINTBUFFER_SIZE+4) + +// size in samples of copy buffer used by pitch shifters in mixing +#if defined(_GAMECONSOLE) +#define TEMP_COPY_BUFFER_SIZE (PAINTBUFFER_MEM_SIZE * 2) +#else +// allow more memory for this on PC for developers to pitch-shift their way through dialog +#define TEMP_COPY_BUFFER_SIZE (PAINTBUFFER_MEM_SIZE * 4) +#endif + +// hard clip input value to -32767 <= y <= 32767 +#define CLIP(x) ((x) > 32767 ? 32767 : ((x) < -32767 ? -32767 : (x))) + +#endif // SND_MIX_BUF_H diff --git a/engine/audio/private/snd_mp3_source.cpp b/engine/audio/private/snd_mp3_source.cpp new file mode 100644 index 0000000..d4b012e --- /dev/null +++ b/engine/audio/private/snd_mp3_source.cpp @@ -0,0 +1,602 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +#include "audio_pch.h" +#include "snd_mp3_source.h" +#include "snd_dma.h" +#include "snd_wave_mixer_mp3.h" +#include "filesystem_engine.h" +#include "utldict.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifndef DEDICATED // have to test this because VPC is forcing us to compile this file. + +// How many bytes initial data bytes of the mp3 should be saved in the soundcache, in addition to the small amount of +// metadata (playbackrate, etc). This will increase memory usage by +// ( N * number-of-precached-mp3-sounds-in-the-whole-game ) at all times, as the soundcache is held in memory. +// +// Right now we're setting this to zero. The IsReadyToMix() logic at the data layer will delay mixing of the sound until +// it arrives. Setting this to anything above zero, however, will allow the sound to start, so it needs to either be +// enough to cover SND_ASYNC_LOOKAHEAD_SECONDS or none at all. +#define MP3_STARTUP_DATA_SIZE_BYTES 0 + +CUtlDict< CSentence *, int> g_PhonemeFileSentences; +bool g_bAllPhonemesLoaded; + +void PhonemeMP3Shutdown( void ) +{ + g_PhonemeFileSentences.PurgeAndDeleteElements(); + g_bAllPhonemesLoaded = false; +} + +void AddPhonemesFromFile( const char *pszFileName ) +{ + // If all Phonemes are loaded, do not load anymore + if ( g_bAllPhonemesLoaded && g_PhonemeFileSentences.Count() != 0 ) + return; + + // Empty file name implies stop loading more phonemes + if ( pszFileName == NULL ) + { + g_bAllPhonemesLoaded = true; + return; + } + + // Load this file + g_bAllPhonemesLoaded = false; + + CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); + if ( g_pFileSystem->ReadFile( pszFileName, "MOD", buf ) ) + { + while ( 1 ) + { + char token[4096]; + buf.GetString( token ); + + V_FixSlashes( token ); + + int iIndex = g_PhonemeFileSentences.Find( token ); + + if ( iIndex != g_PhonemeFileSentences.InvalidIndex() ) + { + delete g_PhonemeFileSentences.Element( iIndex ); + g_PhonemeFileSentences.Remove( token ); + } + + CSentence *pSentence = new CSentence; + g_PhonemeFileSentences.Insert( token, pSentence ); + + buf.GetString( token ); + + if ( strlen( token ) <= 0 ) + break; + + if ( !stricmp( token, "{" ) ) + { + pSentence->InitFromBuffer( buf ); + } + } + } +} + +CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx ) +{ + m_sampleRate = 0; + m_pSfx = pSfx; + m_refCount = 0; + + m_dataStart = 0; + + int file = g_pSndIO->open( pSfx->GetFileName() ); + if ( file != -1 ) + { + m_dataSize = g_pSndIO->size( file ); + g_pSndIO->close( file ); + } + else + { + // No sound cache, the file isn't here, print this so that the relatively deep failure points that are about to + // spew make a little more sense + Warning( "MP3 is completely missing, sound system will be upset to learn of this [ %s ]\n", pSfx->GetFileName() ); + m_dataSize = 0; + } + + + m_nCachedDataSize = 0; + m_bIsPlayOnce = false; + m_bIsSentenceWord = false; + m_bCheckedForPendingSentence = false; +} + +CAudioSourceMP3::CAudioSourceMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) +{ + m_pSfx = pSfx; + m_refCount = 0; + + m_sampleRate = info->SampleRate(); + m_dataSize = info->DataSize(); + m_dataStart = info->DataStart(); + + m_nCachedDataSize = 0; + m_bIsPlayOnce = false; + m_bCheckedForPendingSentence = false; + + CheckAudioSourceCache(); +} + +CAudioSourceMP3::~CAudioSourceMP3() +{ +} + +// mixer's references +void CAudioSourceMP3::ReferenceAdd( CAudioMixer * ) +{ + m_refCount++; +} + +void CAudioSourceMP3::ReferenceRemove( CAudioMixer * ) +{ + m_refCount--; + if ( m_refCount == 0 && IsPlayOnce() ) + { + SetPlayOnce( false ); // in case it gets used again + CacheUnload(); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAudioSourceMP3::IsAsyncLoad() +{ + // If there's a bit of "cached data" then we don't have to lazy/async load (we still async load the remaining data, + // but we run from the cache initially) + return ( m_nCachedDataSize > 0 ) ? false : true; +} + +// check reference count, return true if nothing is referencing this +bool CAudioSourceMP3::CanDelete( void ) +{ + return m_refCount > 0 ? false : true; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : int +//----------------------------------------------------------------------------- +int CAudioSourceMP3::GetType() +{ + return AUDIO_SOURCE_MP3; +} + +//----------------------------------------------------------------------------- +void CAudioSourceMP3::SetSentence( CSentence *pSentence ) +{ + CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet(); + + if ( !info ) + return; + + if ( info && info->Sentence() ) + return; + + CSentence *pNewSentence = new CSentence; + + pNewSentence->Append( 0.0f, *pSentence ); + pNewSentence->MakeRuntimeOnly(); + + info->SetSentence( pNewSentence ); +} + +int CAudioSourceMP3::SampleRate() +{ + if ( !m_sampleRate ) + { + // This should've come from the sound cache. We can avoid sync I/O jank if and only if we've started streaming + // data already for some other reason. (Despite the name, CreateWaveDataMemory is just creating a wrapper class + // that manages access to the wave data cache) + IWaveData *pData = CreateWaveDataMemory( *this ); + if ( !pData->IsReadyToMix() && SND_IsInGame() ) + { + // If you hit this, you're creating a sound source that isn't in the sound cache, and asking for its sample + // rate before it has streamed enough data in to read it from the underlying file. Your options are: + // - Rebuild sound cache or figure out why this sound wasn't included. + // - Precache this sound at level load so this doesn't happen during gameplay. + // - Somehow call CacheLoad() on this source earlier so it has time to get data into memory so the data + // shows up as IsReadyToMix here, and this crutch won't jank. + Warning( "MP3 initialized with no sound cache, this may cause janking. [ %s ]\n", GetFileName() ); + // The code below will still go fine, but the mixer will emit a jank warning that the data wasn't ready and + // do sync I/O + } + CAudioMixerWaveMP3 *pMixer = new CAudioMixerWaveMP3( pData ); + m_sampleRate = pMixer->GetStreamOutputRate(); + // pData ownership is passed to, and free'd by, pMixer + delete pMixer; + } + return m_sampleRate; +} + +void CAudioSourceMP3::GetCacheData( CAudioSourceCachedInfo *info ) +{ + // Don't want to replicate our cached sample rate back into the new cache, ensure we recompute it. + CAudioMixerWaveMP3 *pTempMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) ); + m_sampleRate = pTempMixer->GetStreamOutputRate(); + delete pTempMixer; + + AssertMsg( m_sampleRate, "Creating cache with invalid sample rate data" ); + if ( !m_sampleRate ) + { + Warning( "Failed to find sample rate creating cache data for MP3, cache will be invalid [ %s ]\n", GetFileName() ); + } + + info->SetSampleRate( m_sampleRate ); + info->SetDataStart( 0 ); + + int file = g_pSndIO->open( m_pSfx->GetFileName() ); + if ( !file ) + { + Warning( "Failed to find file for building soundcache [ %s ]\n", m_pSfx->GetFileName() ); + // Don't re-use old cached value + m_dataSize = 0; + } + else + { + m_dataSize = (int)g_pSndIO->size( file ); + } + + Assert( m_dataSize > 0 ); + + // Do we need to actually load any audio data? +#if MP3_STARTUP_DATA_SIZE_BYTES > 0 // We may have defined the startup data to nothingness + if ( info->s_bIsPrecacheSound && m_dataSize > 0 ) + { + // Ideally this would mimic the wave startup data code and figure out this calculation: + // int bytesNeeded = m_channels * ( m_bits >> 3 ) * m_rate * SND_ASYNC_LOOKAHEAD_SECONDS; + // (plus header) + int dataSize = min( MP3_STARTUP_DATA_SIZE_BYTES, m_dataSize ); + byte *data = new byte[ dataSize ](); + int readSize = g_pSndIO->read( data, dataSize, file ); + if ( readSize != dataSize ) + { + Warning( "Building soundcache, expected %i bytes of data but got %i [ %s ]\n", dataSize, readSize, m_pSfx->GetFileName() ); + dataSize = readSize; + } + info->SetCachedDataSize( dataSize ); + info->SetCachedData( data ); + } +#endif // MP3_STARTUP_DATA_SIZE_BYTES > 0 + + g_pSndIO->close( file ); + + // Data size gets computed in GetStartupData!!! + info->SetDataSize( m_dataSize ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +char const *CAudioSourceMP3::GetFileName() +{ + return m_pSfx ? m_pSfx->GetFileName() : "NULL m_pSfx"; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceMP3::CheckAudioSourceCache() +{ + Assert( m_pSfx ); + + if ( !m_pSfx->IsPrecachedSound() ) + { + return; + } + + // This will "re-cache" this if it's not in this level's cache already + m_AudioCacheHandle.Get( GetType(), true, m_pSfx, &m_nCachedDataSize ); +} + +//----------------------------------------------------------------------------- +// Purpose: NULL the wave data pointer (we haven't loaded yet) +//----------------------------------------------------------------------------- +CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx ) : + CAudioSourceMP3( pSfx ) +{ + m_hCache = 0; +} + +CAudioSourceMP3Cache::CAudioSourceMP3Cache( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : + CAudioSourceMP3( pSfx, info ) +{ + m_hCache = 0; + + m_dataSize = info->DataSize(); + m_dataStart = info->DataStart(); + + m_bNoSentence = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Free any wave data we've allocated +//----------------------------------------------------------------------------- +CAudioSourceMP3Cache::~CAudioSourceMP3Cache( void ) +{ + CacheUnload(); +} + +int CAudioSourceMP3Cache::GetCacheStatus( void ) +{ + bool bCacheValid; + int loaded = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ) ? AUDIO_IS_LOADED : AUDIO_NOT_LOADED; + if ( !bCacheValid ) + { + wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(), m_dataSize, m_dataStart ); + } + return loaded; +} + + +void CAudioSourceMP3Cache::CacheLoad( void ) +{ + // Commence lazy load? + if ( m_hCache != 0 ) + { + GetCacheStatus(); + return; + } + + m_hCache = wavedatacache->AsyncLoadCache( m_pSfx->GetFileName(), m_dataSize, m_dataStart ); +} + +void CAudioSourceMP3Cache::CacheUnload( void ) +{ + if ( m_hCache != 0 ) + { + wavedatacache->Unload( m_hCache ); + } +} + +char *CAudioSourceMP3Cache::GetDataPointer( void ) +{ + char *pMP3Data = NULL; + bool dummy = false; + + if ( m_hCache == 0 ) + { + CacheLoad(); + } + + wavedatacache->GetDataPointer( + m_hCache, + m_pSfx->GetFileName(), + m_dataSize, + m_dataStart, + (void **)&pMP3Data, + 0, + &dummy ); + + return pMP3Data; +} + +int CAudioSourceMP3Cache::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + // how many bytes are available ? + int totalSampleCount = m_dataSize - samplePosition; + + // may be asking for a sample out of range, clip at zero + if ( totalSampleCount < 0 ) + totalSampleCount = 0; + + // clip max output samples to max available + if ( sampleCount > totalSampleCount ) + sampleCount = totalSampleCount; + + // if we are returning some samples, store the pointer + if ( sampleCount ) + { + // Starting past end of "preloaded" data, just use regular cache + if ( samplePosition >= m_nCachedDataSize ) + { + *pData = GetDataPointer(); + } + else + { + // Start async loader if we haven't already done so + CacheLoad(); + + // Return less data if we are about to run out of uncached data + if ( samplePosition + sampleCount >= m_nCachedDataSize ) + { + sampleCount = m_nCachedDataSize - samplePosition; + } + + // Point at preloaded/cached data from .cache file for now + *pData = GetCachedDataPointer(); + } + + if ( *pData ) + { + *pData = (char *)*pData + samplePosition; + } + else + { + // Out of data or file i/o problem + sampleCount = 0; + } + } + + return sampleCount; +} + +CAudioMixer *CAudioSourceMP3Cache::CreateMixer( int initialStreamPosition ) +{ + CAudioMixer *pMixer = new CAudioMixerWaveMP3( CreateWaveDataMemory(*this) ); + + return pMixer; +} + +CSentence *CAudioSourceMP3Cache::GetSentence( void ) +{ + // Already checked and this wav doesn't have sentence data... + if ( m_bNoSentence == true ) + { + return NULL; + } + + // Look up sentence from cache + CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet(); + if ( !info ) + { + info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + } + Assert( info ); + if ( !info ) + { + m_bNoSentence = true; + return NULL; + } + + CSentence *sentence = info->Sentence(); + if ( !sentence ) + { + if ( !m_bCheckedForPendingSentence ) + { + int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() ); + if ( iSentence != g_PhonemeFileSentences.InvalidIndex() ) + { + sentence = g_PhonemeFileSentences.Element( iSentence ); + SetSentence( sentence ); + } + m_bCheckedForPendingSentence = true; + } + } + + if ( !sentence ) + { + m_bNoSentence = true; + return NULL; + } + + if ( sentence->m_bIsValid ) + { + return sentence; + } + + m_bNoSentence = true; + + return NULL; +} + +//----------------------------------------------------------------------------- +// CAudioSourceStreamMP3 +//----------------------------------------------------------------------------- +CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx ) : + CAudioSourceMP3( pSfx ) +{ +} + +CAudioSourceStreamMP3::CAudioSourceStreamMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : + CAudioSourceMP3( pSfx, info ) +{ + m_dataSize = info->DataSize(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceStreamMP3::Prefetch() +{ + PrefetchDataStream( m_pSfx->GetFileName(), 0, m_dataSize ); +} + +CAudioMixer *CAudioSourceStreamMP3::CreateMixer( int intialStreamPosition ) +{ + // BUGBUG: Source constructs the IWaveData, mixer frees it, fix this? + IWaveData *pWaveData = CreateWaveDataStream( *this, static_cast<IWaveStreamSource *>( this ), m_pSfx->GetFileName(), 0, m_dataSize, m_pSfx, 0 ); + if ( pWaveData ) + { + CAudioMixer *pMixer = new CAudioMixerWaveMP3( pWaveData ); + if ( pMixer ) + { + if ( !m_bCheckedForPendingSentence ) + { + int iSentence = g_PhonemeFileSentences.Find( m_pSfx->GetFileName() ); + + if ( iSentence != g_PhonemeFileSentences.InvalidIndex() ) + { + SetSentence( g_PhonemeFileSentences.Element( iSentence ) ); + } + + m_bCheckedForPendingSentence = true; + } + + return pMixer; + } + + // no mixer but pWaveData was deleted in mixer's destructor + // so no need to delete + } + + return NULL; +} + +int CAudioSourceStreamMP3::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + return 0; +} + +bool Audio_IsMP3( const char *pName ) +{ + int len = strlen(pName); + if ( len > 4 ) + { + if ( !Q_strnicmp( &pName[len - 4], ".mp3", 4 ) ) + { + return true; + } + } + return false; +} + + +CAudioSource *Audio_CreateStreamedMP3( CSfxTable *pSfx ) +{ + CAudioSourceStreamMP3 *pMP3 = NULL; + CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx ); + if ( info ) + { + pMP3 = new CAudioSourceStreamMP3( pSfx, info ); + } + else + { + pMP3 = new CAudioSourceStreamMP3( pSfx ); + } + return pMP3; +} + + +CAudioSource *Audio_CreateMemoryMP3( CSfxTable *pSfx ) +{ + CAudioSourceMP3Cache *pMP3 = NULL; + CAudioSourceCachedInfo *info = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_MP3, pSfx->IsPrecachedSound(), pSfx ); + if ( info ) + { + pMP3 = new CAudioSourceMP3Cache( pSfx, info ); + } + else + { + pMP3 = new CAudioSourceMP3Cache( pSfx ); + } + return pMP3; +} + +#endif + diff --git a/engine/audio/private/snd_mp3_source.h b/engine/audio/private/snd_mp3_source.h new file mode 100644 index 0000000..38d6291 --- /dev/null +++ b/engine/audio/private/snd_mp3_source.h @@ -0,0 +1,186 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_MP3_SOURCE_H +#define SND_MP3_SOURCE_H +#ifdef _WIN32 +#pragma once +#endif + +#include "snd_audio_source.h" +#include "snd_wave_data.h" +#include "snd_sfx.h" + +class IWaveData; +class CAudioMixer; + +abstract_class CAudioSourceMP3 : public CAudioSource +{ +public: + + CAudioSourceMP3( CSfxTable *pSfx ); + CAudioSourceMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + virtual ~CAudioSourceMP3(); + + // Create an instance (mixer) of this audio source + virtual CAudioMixer *CreateMixer( int initialStreamPosition = 0 ) = 0; + + virtual int GetType( void ); + virtual void GetCacheData( CAudioSourceCachedInfo *info ); + + // Provide samples for the mixer. You can point pData at your own data, or if you prefer to copy the data, + // you can copy it into copyBuf and set pData to copyBuf. + virtual int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) = 0; + + virtual int SampleRate( void ); + + // Returns true if the source is a voice source. + // This affects the voice_overdrive behavior (all sounds get quieter when + // someone is speaking). + virtual bool IsVoiceSource() { return false; } + virtual int SampleSize( void ) { return 1; } + + // Total number of samples in this source. NOTE: Some sources are infinite (mic input), they should return + // a count equal to one second of audio at their current rate. + virtual int SampleCount( void ) { return m_dataSize; } + + virtual int Format() { return 0; } + virtual int DataSize( void ) { return 0; } + + virtual bool IsLooped( void ) { return false; } + virtual bool IsStereoWav( void ) { return false; } + virtual bool IsStreaming( void ) { return false; } + virtual int GetCacheStatus( void ) { return AUDIO_IS_LOADED; } + virtual void CacheLoad( void ) {} + virtual void CacheUnload( void ) {} + virtual CSentence *GetSentence( void ) { return NULL; } + + virtual int ZeroCrossingBefore( int sample ) { return sample; } + virtual int ZeroCrossingAfter( int sample ) { return sample; } + + // mixer's references + virtual void ReferenceAdd( CAudioMixer *pMixer ); + virtual void ReferenceRemove( CAudioMixer *pMixer ); + // check reference count, return true if nothing is referencing this + virtual bool CanDelete( void ); + + virtual bool IsAsyncLoad(); + + virtual void CheckAudioSourceCache(); + + virtual char const *GetFileName(); + + virtual void SetPlayOnce( bool isPlayOnce ) { m_bIsPlayOnce = isPlayOnce; } + virtual bool IsPlayOnce() { return m_bIsPlayOnce; } + + virtual void SetSentenceWord( bool bIsWord ) { m_bIsSentenceWord = bIsWord; } + virtual bool IsSentenceWord() { return m_bIsSentenceWord; } + + virtual int SampleToStreamPosition( int samplePosition ) { return 0; } + virtual int StreamToSamplePosition( int streamPosition ) { return 0; } + + virtual void SetSentence( CSentence *pSentence ); + +protected: + + //----------------------------------------------------------------------------- + // Purpose: + // Output : byte + //----------------------------------------------------------------------------- + inline byte *GetCachedDataPointer() + { + VPROF("CAudioSourceMP3::GetCachedDataPointer"); + + CAudioSourceCachedInfo *info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_MP3, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + if ( !info ) + { + Assert( !"CAudioSourceMP3::GetCachedDataPointer info == NULL" ); + return NULL; + } + + return (byte *)info->CachedData(); + } + + CAudioSourceCachedInfoHandle_t m_AudioCacheHandle; + int m_nCachedDataSize; + +protected: + CSfxTable *m_pSfx; + int m_sampleRate; + int m_dataSize; + int m_dataStart; + int m_refCount; + bool m_bIsPlayOnce : 1; + bool m_bIsSentenceWord : 1; + bool m_bCheckedForPendingSentence; +}; + +//----------------------------------------------------------------------------- +// Purpose: Streaming MP3 file +//----------------------------------------------------------------------------- +class CAudioSourceStreamMP3 : public CAudioSourceMP3, public IWaveStreamSource +{ +public: + CAudioSourceStreamMP3( CSfxTable *pSfx ); + CAudioSourceStreamMP3( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + ~CAudioSourceStreamMP3() {} + + bool IsStreaming( void ) OVERRIDE { return true; } + bool IsStereoWav( void ) OVERRIDE { return false; } + CAudioMixer *CreateMixer( int initialStreamPosition = 0 ) OVERRIDE; + int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) OVERRIDE; + + // IWaveStreamSource + int UpdateLoopingSamplePosition( int samplePosition ) OVERRIDE + { + return samplePosition; + } + void UpdateSamples( char *pData, int sampleCount ) OVERRIDE {} + + int GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples ) OVERRIDE + { + return 0; + } + + void Prefetch() OVERRIDE; + +private: + CAudioSourceStreamMP3( const CAudioSourceStreamMP3 & ); // not implemented, not accessible +}; + +class CAudioSourceMP3Cache : public CAudioSourceMP3 +{ +public: + CAudioSourceMP3Cache( CSfxTable *pSfx ); + CAudioSourceMP3Cache( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + ~CAudioSourceMP3Cache( void ); + + int GetCacheStatus( void ) OVERRIDE; + void CacheLoad( void ) OVERRIDE; + void CacheUnload( void ) OVERRIDE; + // NOTE: "samples" are bytes for MP3 + int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) OVERRIDE; + CAudioMixer *CreateMixer( int initialStreamPosition = 0 ) OVERRIDE; + CSentence *GetSentence( void ) OVERRIDE; + + void Prefetch() OVERRIDE {} + +protected: + virtual char *GetDataPointer( void ); + memhandle_t m_hCache; + +private: + CAudioSourceMP3Cache( const CAudioSourceMP3Cache & ); + + unsigned int m_bNoSentence : 1; +}; + +bool Audio_IsMP3( const char *pName ); +CAudioSource *Audio_CreateStreamedMP3( CSfxTable *pSfx ); +CAudioSource *Audio_CreateMemoryMP3( CSfxTable *pSfx ); + +#endif // SND_MP3_SOURCE_H diff --git a/engine/audio/private/snd_posix.cpp b/engine/audio/private/snd_posix.cpp new file mode 100644 index 0000000..629105b --- /dev/null +++ b/engine/audio/private/snd_posix.cpp @@ -0,0 +1,15 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//===========================================================================// + +#include "audio_pch.h" +#include "ivoicerecord.h" +#include "voice_mixer_controls.h" +#include "snd_dev_openal.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + diff --git a/engine/audio/private/snd_sentence_mixer.cpp b/engine/audio/private/snd_sentence_mixer.cpp new file mode 100644 index 0000000..7a5fd13 --- /dev/null +++ b/engine/audio/private/snd_sentence_mixer.cpp @@ -0,0 +1,369 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Sentence Mixing +// +//=============================================================================// + +#include "audio_pch.h" +#include "vox_private.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//----------------------------------------------------------------------------- +// Purpose: This replaces the old sentence logic that was integrated with the +// sound code. Now it is a hierarchical mixer. +//----------------------------------------------------------------------------- +class CSentenceMixer : public CAudioMixer +{ +public: + CSentenceMixer( voxword_t *pWords ); + ~CSentenceMixer( void ); + + // return number of samples mixed + virtual int MixDataToDevice( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ); + virtual int SkipSamples( channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ); + virtual bool ShouldContinueMixing( void ); + + virtual CAudioSource* GetSource( void ); + + // get the current position (next sample to be mixed) + virtual int GetSamplePosition( void ); + virtual float ModifyPitch( float pitch ); + virtual float GetVolumeScale( void ); + + // BUGBUG: These are only applied to the current word, not the whole sentence!!!! + virtual void SetSampleStart( int newPosition ); + virtual void SetSampleEnd( int newEndPosition ); + + virtual void SetStartupDelaySamples( int delaySamples ); + virtual int GetMixSampleSize() { return m_pCurrentWordMixer ? m_pCurrentWordMixer->GetMixSampleSize() : 0; } + + virtual bool IsReadyToMix(); + + virtual int GetPositionForSave() { return GetSamplePosition(); } + virtual void SetPositionFromSaved( int savedPosition ) { SetSampleStart( savedPosition ); } + +private: + CAudioMixer *LoadWord( int nWordIndex ); + void FreeWord( int nWordIndex ); + + // identifies the active word + int m_currentWordIndex; + CAudioMixer *m_pCurrentWordMixer; + + // set when a transition to a new word occurs + bool m_bNewWord; + + voxword_t m_VoxWords[CVOXWORDMAX]; + CAudioMixer *m_pWordMixers[CVOXWORDMAX]; + int m_nNumWords; +}; + +CAudioMixer *CreateSentenceMixer( voxword_t *pWords ) +{ + if ( pWords ) + { + return new CSentenceMixer( pWords ); + } + + return NULL; +} + +CSentenceMixer::CSentenceMixer( voxword_t *pWords ) +{ + // count the expected number of words + m_nNumWords = 0; + while ( pWords[m_nNumWords].sfx != NULL ) + { + // get a private copy of the words + m_VoxWords[m_nNumWords] = pWords[m_nNumWords]; + m_nNumWords++; + if ( m_nNumWords >= ARRAYSIZE( m_VoxWords ) ) + { + // very long sentence, prevent overflow + break; + } + } + + // startup all the mixers now, this serves as a hint to the audio streamer + // actual mixing will commence when they are ALL ready + for ( int nWord = 0; nWord < m_nNumWords; nWord++ ) + { + // it is possible to get a null mixer (due to wav error, etc) + // the sentence will skip these words + m_pWordMixers[nWord] = LoadWord( nWord ); + } + Assert( m_nNumWords < ARRAYSIZE( m_pWordMixers ) ); + + // find first valid word mixer + m_currentWordIndex = 0; + m_pCurrentWordMixer = NULL; + for ( int nWord = 0; nWord < m_nNumWords; nWord++ ) + { + if ( m_pWordMixers[nWord] ) + { + m_currentWordIndex = nWord; + m_pCurrentWordMixer = m_pWordMixers[nWord]; + break; + } + } + + m_bNewWord = ( m_pCurrentWordMixer != NULL ); +} + +CSentenceMixer::~CSentenceMixer( void ) +{ + // free all words + for ( int nWord = 0; nWord < m_nNumWords; nWord++ ) + { + FreeWord( nWord ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true if mixing can commence, false otherwise +//----------------------------------------------------------------------------- +bool CSentenceMixer::IsReadyToMix() +{ + if ( !m_pCurrentWordMixer ) + { + // no word, but mixing has to commence in order to shutdown + return true; + } + + // all the words should be available before mixing the sentence + for ( int nWord = m_currentWordIndex; nWord < m_nNumWords; nWord++ ) + { + if ( m_pWordMixers[nWord] && !m_pWordMixers[nWord]->IsReadyToMix() ) + { + // Still waiting for async data to arrive + return false; + } + } + + if ( m_bNewWord ) + { + m_bNewWord = false; + + int start = m_VoxWords[m_currentWordIndex].start; + int end = m_VoxWords[m_currentWordIndex].end; + + // don't allow overlapped ranges + if ( end <= start ) + { + end = 0; + } + + if ( start || end ) + { + int sampleCount = m_pCurrentWordMixer->GetSource()->SampleCount(); + if ( start > 0 && start < 100 ) + { + m_pCurrentWordMixer->SetSampleStart( (int)(sampleCount * 0.01f * start) ); + } + if ( end > 0 && end < 100 ) + { + m_pCurrentWordMixer->SetSampleEnd( (int)(sampleCount * 0.01f * end) ); + } + } + } + + return true; +} + +bool CSentenceMixer::ShouldContinueMixing( void ) +{ + if ( m_pCurrentWordMixer ) + { + // keep mixing until the words run out + return true; + } + + return false; +} + +CAudioSource *CSentenceMixer::GetSource( void ) +{ + if ( m_pCurrentWordMixer ) + { + return m_pCurrentWordMixer->GetSource(); + } + + return NULL; +} + +// get the current position (next sample to be mixed) +int CSentenceMixer::GetSamplePosition( void ) +{ + if ( m_pCurrentWordMixer ) + { + return m_pCurrentWordMixer->GetSamplePosition(); + } + + return 0; +} + +void CSentenceMixer::SetSampleStart( int newPosition ) +{ + if ( m_pCurrentWordMixer ) + { + m_pCurrentWordMixer->SetSampleStart( newPosition ); + } +} + +// End playback at newEndPosition +void CSentenceMixer::SetSampleEnd( int newEndPosition ) +{ + if ( m_pCurrentWordMixer ) + { + m_pCurrentWordMixer->SetSampleEnd( newEndPosition ); + } +} + +void CSentenceMixer::SetStartupDelaySamples( int delaySamples ) +{ + if ( m_pCurrentWordMixer ) + { + m_pCurrentWordMixer->SetStartupDelaySamples( delaySamples ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Free a word +//----------------------------------------------------------------------------- +void CSentenceMixer::FreeWord( int nWord ) +{ + if ( m_pWordMixers[nWord] ) + { + delete m_pWordMixers[nWord]; + m_pWordMixers[nWord] = NULL; + } + + if ( m_VoxWords[nWord].sfx ) + { + // If this wave wasn't precached by the game code + if ( !m_VoxWords[nWord].fKeepCached ) + { + // If this was the last mixer that had a reference + if ( m_VoxWords[nWord].sfx->pSource->CanDelete() ) + { + // free the source + delete m_VoxWords[nWord].sfx->pSource; + m_VoxWords[nWord].sfx->pSource = NULL; + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Load a word +//----------------------------------------------------------------------------- +CAudioMixer *CSentenceMixer::LoadWord( int nWord ) +{ + CAudioMixer *pMixer = NULL; + if ( m_VoxWords[nWord].sfx ) + { + CAudioSource *pSource = S_LoadSound( m_VoxWords[nWord].sfx, NULL ); + if ( pSource ) + { + pSource->SetSentenceWord( true ); + pMixer = pSource->CreateMixer(); + } + } + + return pMixer; +} + +float CSentenceMixer::ModifyPitch( float pitch ) +{ + if ( m_pCurrentWordMixer ) + { + if ( m_VoxWords[m_currentWordIndex].pitch > 0 ) + { + pitch += (m_VoxWords[m_currentWordIndex].pitch - 100) * 0.01f; + } + } + return pitch; +} + +float CSentenceMixer::GetVolumeScale( void ) +{ + if ( m_pCurrentWordMixer ) + { + if ( m_VoxWords[m_currentWordIndex].volume ) + { + float volume = m_VoxWords[m_currentWordIndex].volume * 0.01; + if ( volume < 1.0f ) + return volume; + } + } + return 1.0f; +} + +int CSentenceMixer::SkipSamples( channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) +{ + Assert( 0 ); + return 0; +} + +// return number of samples mixed +int CSentenceMixer::MixDataToDevice( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) +{ + if ( !m_pCurrentWordMixer ) + { + return 0; + } + + // save this to compute total output + int startingOffset = outputOffset; + + while ( sampleCount > 0 && m_pCurrentWordMixer ) + { + int outputCount = m_pCurrentWordMixer->MixDataToDevice( pDevice, pChannel, sampleCount, outputRate, outputOffset ); + + outputOffset += outputCount; + sampleCount -= outputCount; + + if ( !m_pCurrentWordMixer->ShouldContinueMixing() ) + { + bool bMouth = SND_IsMouth( pChannel ); + if ( bMouth ) + { + SND_ClearMouth( pChannel ); + } + + // advance to next valid word mixer + do + { + m_currentWordIndex++; + if ( m_currentWordIndex >= m_nNumWords ) + { + // end of sentence + m_pCurrentWordMixer = NULL; + break; + } + m_pCurrentWordMixer = m_pWordMixers[m_currentWordIndex]; + } + while ( m_pCurrentWordMixer == NULL ); + + if ( m_pCurrentWordMixer ) + { + m_bNewWord = true; + + pChannel->sfx = m_VoxWords[m_currentWordIndex].sfx; + if ( bMouth ) + { + SND_UpdateMouth( pChannel ); + } + if ( !IsReadyToMix() ) + { + // current word isn't ready, stop mixing + break; + } + } + } + } + + return outputOffset - startingOffset; +} diff --git a/engine/audio/private/snd_sfx.h b/engine/audio/private/snd_sfx.h new file mode 100644 index 0000000..a0de0d3 --- /dev/null +++ b/engine/audio/private/snd_sfx.h @@ -0,0 +1,48 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_SFX_H +#define SND_SFX_H + +#if defined( _WIN32 ) +#pragma once +#endif + +class CAudioSource; + +class CSfxTable +{ +public: + CSfxTable(); + + // gets sound name, possible decoracted with prefixes + virtual const char *getname(); + // gets the filename, the part after the optional prefixes + const char *GetFileName(); + FileNameHandle_t GetFileNameHandle(); + + void SetNamePoolIndex( int index ); + bool IsPrecachedSound(); + void OnNameChanged( const char *pName ); + + int m_namePoolIndex; + CAudioSource *pSource; + + bool m_bUseErrorFilename : 1; + bool m_bIsUISound : 1; + bool m_bIsLateLoad : 1; + bool m_bMixGroupsCached : 1; + byte m_mixGroupCount; + // UNDONE: Use a fixed bit vec here? + byte m_mixGroupList[8]; + +private: + // Only set in debug mode so you can see the name. + const char *m_pDebugName; +}; + +#endif // SND_SFX_H diff --git a/engine/audio/private/snd_stubs.cpp b/engine/audio/private/snd_stubs.cpp new file mode 100644 index 0000000..9adb60a --- /dev/null +++ b/engine/audio/private/snd_stubs.cpp @@ -0,0 +1,279 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// + +#include "audio_pch.h" + +//#include "matchmaking/IMatchFramework.h" +#include "tier2/tier2.h" + +#include "audio/public/voice.h" + +#if !defined( DEDICATED ) && ( defined( OSX ) || defined( _WIN32 ) ) && !defined( NO_STEAM ) +#include "cl_steamauth.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CEngineVoiceStub *Audio_GetEngineVoiceStub() +{ + static CEngineVoiceStub s_EngineVoiceStub; + return &s_EngineVoiceStub; +} + + +#if !defined( DEDICATED ) && ( defined( OSX ) || defined( _WIN32 ) ) && !defined( NO_STEAM ) + +class CEngineVoiceSteam : public IEngineVoice +{ +public: + CEngineVoiceSteam(); + +public: + virtual bool IsHeadsetPresent( int iController ); + virtual bool IsLocalPlayerTalking( int iController ); + + virtual void AddPlayerToVoiceList( XUID xPlayer, int iController ); + virtual void RemovePlayerFromVoiceList( XUID xPlayer, int iController ); + + virtual void GetRemoteTalkers( int *pNumTalkers, XUID *pRemoteTalkers ); + + virtual bool VoiceUpdateData( int iController ); + virtual void GetVoiceData( int iController, const byte **ppvVoiceDataBuffer, unsigned int *pnumVoiceDataBytes ); + virtual void VoiceResetLocalData( int iController ); + + virtual void SetPlaybackPriority( XUID remoteTalker, int iController, int iAllowPlayback ); + virtual void PlayIncomingVoiceData( XUID xuid, const byte *pbData, unsigned int dwDataSize, const bool *bAudiblePlayers = NULL ); + + virtual void RemoveAllTalkers(); + +protected: + void AudioInitializationUpdate(); + +public: + bool m_bLocalVoice[ XUSER_MAX_COUNT ]; + CUtlVector< XUID > m_arrRemoteVoice; + bool m_bInitializedAudio; + byte m_pbVoiceData[ 1024 * XUSER_MAX_COUNT ]; +}; + +CEngineVoiceSteam::CEngineVoiceSteam() +{ + memset( m_bLocalVoice, 0, sizeof( m_bLocalVoice ) ); + memset( m_pbVoiceData, 0, sizeof( m_pbVoiceData ) ); + m_bInitializedAudio = false; +} + +bool CEngineVoiceSteam::IsHeadsetPresent( int iController ) +{ + return false; +} + +bool CEngineVoiceSteam::IsLocalPlayerTalking( int iController ) +{ + uint32 nBytes = 0; + EVoiceResult res = Steam3Client().SteamUser()->GetAvailableVoice( &nBytes, NULL, 0 ); + switch ( res ) + { + case k_EVoiceResultOK: + case k_EVoiceResultNoData: + case k_EVoiceResultBufferTooSmall: + return true; + default: + return false; + } +} + +void CEngineVoiceSteam::AddPlayerToVoiceList( XUID xPlayer, int iController ) +{ + if ( !xPlayer && iController >= 0 && iController < XUSER_MAX_COUNT ) + { + // Add local player + m_bLocalVoice[ iController ] = true; + AudioInitializationUpdate(); + } + + if ( xPlayer ) + { + if ( m_arrRemoteVoice.Find( xPlayer ) == m_arrRemoteVoice.InvalidIndex() ) + { + m_arrRemoteVoice.AddToTail( xPlayer ); + AudioInitializationUpdate(); + } + } +} + +void CEngineVoiceSteam::RemovePlayerFromVoiceList( XUID xPlayer, int iController ) +{ + if ( !xPlayer && iController >= 0 && iController < XUSER_MAX_COUNT ) + { + // Remove local player + m_bLocalVoice[ iController ] = false; + AudioInitializationUpdate(); + } + + if ( xPlayer ) + { + int idx = m_arrRemoteVoice.Find( xPlayer ); + if ( idx != m_arrRemoteVoice.InvalidIndex() ) + { + m_arrRemoteVoice.FastRemove( idx ); + AudioInitializationUpdate(); + } + } +} + +void CEngineVoiceSteam::GetRemoteTalkers( int *pNumTalkers, XUID *pRemoteTalkers ) +{ + if ( pNumTalkers ) + *pNumTalkers = m_arrRemoteVoice.Count(); + + if ( pRemoteTalkers ) + { + for ( int k = 0; k < m_arrRemoteVoice.Count(); ++ k ) + pRemoteTalkers[k] = m_arrRemoteVoice[k]; + } +} + +bool CEngineVoiceSteam::VoiceUpdateData( int iController ) +{ + uint32 nBytes = 0; + EVoiceResult res = Steam3Client().SteamUser()->GetAvailableVoice( &nBytes, NULL, 0 ); + switch ( res ) + { + case k_EVoiceResultOK: + // case k_EVoiceResultNoData: - no data means false + case k_EVoiceResultBufferTooSmall: + return true; + default: + return false; + } +} + +void CEngineVoiceSteam::GetVoiceData( int iController, const byte **ppvVoiceDataBuffer, unsigned int *pnumVoiceDataBytes ) +{ + const int size = ARRAYSIZE( m_pbVoiceData ) / XUSER_MAX_COUNT; + byte *pbVoiceData = m_pbVoiceData + iController * ARRAYSIZE( m_pbVoiceData ) / XUSER_MAX_COUNT; + *ppvVoiceDataBuffer = pbVoiceData; + + EVoiceResult res = Steam3Client().SteamUser()->GetVoice( true, pbVoiceData, size, pnumVoiceDataBytes, false, NULL, 0, NULL, 0 ); + switch ( res ) + { + case k_EVoiceResultNoData: + case k_EVoiceResultOK: + return; + default: + *pnumVoiceDataBytes = 0; + *ppvVoiceDataBuffer = NULL; + return; + } +} + +void CEngineVoiceSteam::VoiceResetLocalData( int iController ) +{ + const int size = ARRAYSIZE( m_pbVoiceData ) / XUSER_MAX_COUNT; + byte *pbVoiceData = m_pbVoiceData + iController * ARRAYSIZE( m_pbVoiceData ) / XUSER_MAX_COUNT; + memset( pbVoiceData, 0, size ); +} + +void CEngineVoiceSteam::SetPlaybackPriority( XUID remoteTalker, int iController, int iAllowPlayback ) +{ + ; +} + +void CEngineVoiceSteam::PlayIncomingVoiceData( XUID xuid, const byte *pbData, unsigned int dwDataSize, const bool *bAudiblePlayers /* = NULL */ ) +{ + for ( DWORD dwSlot = 0; dwSlot < XBX_GetNumGameUsers(); ++ dwSlot ) + { + IPlayerLocal *pPlayer = g_pMatchFramework->GetMatchSystem()->GetPlayerManager()->GetLocalPlayer( dwSlot ); + if ( pPlayer && pPlayer->GetXUID() == xuid ) + //Hack: Don't play stuff that comes from ourselves. + return; + } + + // Make sure voice playback is allowed for the specified user + if ( !g_pMatchFramework->GetMatchSystem()->GetMatchVoice()->CanPlaybackTalker( xuid ) ) + return; + + // Uncompress the voice data + char pbUncompressedVoice[ 11025 * 2 ]; + uint32 numUncompressedBytes; + EVoiceResult res = Steam3Client().SteamUser()->DecompressVoice( const_cast< byte * >( pbData ), dwDataSize, + pbUncompressedVoice, sizeof( pbUncompressedVoice ), &numUncompressedBytes, 0 ); + + if ( res != k_EVoiceResultOK ) + return; + + // Voice channel index + int idxVoiceChan = 0; + int idxRemoteTalker = m_arrRemoteVoice.Find( xuid ); + if ( idxRemoteTalker != m_arrRemoteVoice.InvalidIndex() ) + idxVoiceChan = idxRemoteTalker; + + int nChannel = Voice_GetChannel( idxVoiceChan ); + if ( nChannel == VOICE_CHANNEL_ERROR ) + { + // Create a channel in the voice engine and a channel in the sound engine for this guy. + nChannel = Voice_AssignChannel( idxVoiceChan, false, 0.0f ); + } + + // Give the voice engine the data (it in turn gives it to the mixer for the sound engine). + if ( nChannel != VOICE_CHANNEL_ERROR ) + { + Voice_AddIncomingData( nChannel, pbUncompressedVoice, numUncompressedBytes, 0, false ); + } +} + +void CEngineVoiceSteam::RemoveAllTalkers() +{ + memset( m_bLocalVoice, 0, sizeof( m_bLocalVoice ) ); + m_arrRemoteVoice.RemoveAll(); + AudioInitializationUpdate(); +} + +void CEngineVoiceSteam::AudioInitializationUpdate() +{ + bool bHasTalkers = ( m_arrRemoteVoice.Count() > 0 ); + for ( int k = 0; k < ARRAYSIZE( m_bLocalVoice ); ++ k ) + { + if ( m_bLocalVoice[k] ) + { + bHasTalkers = true; + break; + } + } + + // Initialized already + if ( bHasTalkers == m_bInitializedAudio ) + return; + + // Clear out voice buffers + memset( m_pbVoiceData, 0, sizeof( m_pbVoiceData ) ); + + // Init or deinit voice system + if ( bHasTalkers ) + { + Voice_ForceInit(); + } + else + { + Voice_Deinit(); + } + + m_bInitializedAudio = bHasTalkers; +} + + +IEngineVoice *Audio_GetEngineVoiceSteam() +{ + static CEngineVoiceSteam s_EngineVoiceSteam; + return &s_EngineVoiceSteam; +} + +#else + +IEngineVoice *Audio_GetEngineVoiceSteam() +{ + return Audio_GetEngineVoiceStub(); +} + +#endif
\ No newline at end of file diff --git a/engine/audio/private/snd_stubs.h b/engine/audio/private/snd_stubs.h new file mode 100644 index 0000000..9435813 --- /dev/null +++ b/engine/audio/private/snd_stubs.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// + +#ifndef SND_STUBS_H +#define SND_STUBS_H + +#include "engine/ienginevoice.h" + +class CEngineVoiceStub : public IEngineVoice +{ +public: + virtual bool IsHeadsetPresent( int iController ) { return false; } + virtual bool IsLocalPlayerTalking( int iController ) { return false; } + + virtual void AddPlayerToVoiceList( XUID xPlayer, int iController ) {} + virtual void RemovePlayerFromVoiceList( XUID xPlayer, int iController ) {} + + virtual void GetRemoteTalkers( int *pNumTalkers, XUID *pRemoteTalkers ) + { + if ( pNumTalkers ) + *pNumTalkers = 0; + } + + virtual bool VoiceUpdateData( int iController ) { return false; } + virtual void GetVoiceData( int iController, const byte **ppvVoiceDataBuffer, unsigned int *pnumVoiceDataBytes ) + { + if ( ppvVoiceDataBuffer ) + *ppvVoiceDataBuffer = NULL; + if ( pnumVoiceDataBytes ) + *pnumVoiceDataBytes = NULL; + } + virtual void VoiceResetLocalData( int iController ) {} + + virtual void SetPlaybackPriority( XUID remoteTalker, int iController, int iAllowPlayback ) {} + virtual void PlayIncomingVoiceData( XUID xuid, const byte *pbData, unsigned int dwDataSize, const bool *bAudiblePlayers = NULL ) {} + + virtual void RemoveAllTalkers() {} +}; + +CEngineVoiceStub *Audio_GetEngineVoiceStub(); + + +IEngineVoice *Audio_GetEngineVoiceSteam(); + + +#endif diff --git a/engine/audio/private/snd_wave_data.cpp b/engine/audio/private/snd_wave_data.cpp new file mode 100644 index 0000000..c03b524 --- /dev/null +++ b/engine/audio/private/snd_wave_data.cpp @@ -0,0 +1,2394 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "audio_pch.h" +#include "datacache/idatacache.h" +#include "utllinkedlist.h" +#include "utldict.h" +#include "filesystem/IQueuedLoader.h" +#include "cdll_int.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern IVEngineClient *engineClient; +extern IFileSystem *g_pFileSystem; +extern IDataCache *g_pDataCache; +extern double realtime; + +// console streaming buffer implementation, appropriate for high latency and low memory +// shift this many buffers through the wave +#define STREAM_BUFFER_COUNT 2 +// duration of audio samples per buffer, 200ms is 2x the worst frame rate (10Hz) +// the engine then has at least 400ms to deliver a new buffer or pop (assuming 2 buffers) +#define STREAM_BUFFER_TIME 0.200f +// force a single buffer when streaming waves smaller than this +#define STREAM_BUFFER_DATASIZE XBOX_DVD_ECC_SIZE + +// PC single buffering implementation +// UNDONE: Allocate this in cache instead? +#define SINGLE_BUFFER_SIZE 16384 + +// Force a small cache for debugging cache issues. +// #define FORCE_SMALL_MEMORY_CACHE_SIZE ( 6 * 1024 * 1024 ) + +#define DEFAULT_WAV_MEMORY_CACHE ( 16 * 1024 * 1024 ) +#define DEFAULT_XBOX_WAV_MEMORY_CACHE ( 16 * 1024 * 1024 ) +#define TF_XBOX_WAV_MEMORY_CACHE ( 24 * 1024 * 1024 ) // Team Fortress uses a larger cache + +// Dev builds will be missing soundcaches and hitch sometimes, we only care if its being properly launched from steam where sound caches should be complete. +ConVar snd_async_spew_blocking( "snd_async_spew_blocking", "1", 0, "Spew message to console any time async sound loading blocks on file i/o. ( 0=Off, 1=With -steam only, 2=Always" ); +ConVar snd_async_spew( "snd_async_spew", "0", 0, "Spew all async sound reads, including success" ); +ConVar snd_async_fullyasync( "snd_async_fullyasync", "0", 0, "All playback is fully async (sound doesn't play until data arrives)." ); +ConVar snd_async_stream_spew( "snd_async_stream_spew", "0", 0, "Spew streaming info ( 0=Off, 1=streams, 2=buffers" ); + +static bool SndAsyncSpewBlocking() +{ + int pref = snd_async_spew_blocking.GetInt(); + return ( pref >= 2 ) || ( pref == 1 && CommandLine()->FindParm( "-steam" ) != 0 ); +} + +#define SndAlignReads() 1 + +void MaybeReportMissingWav( char const *wav ); + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +struct asyncwaveparams_t +{ + asyncwaveparams_t() : bPrefetch( false ), bCanBeQueued( false ) {} + + FileNameHandle_t hFilename; // handle to sound item name (i.e. not with sound\ prefix) + int datasize; + int seekpos; + int alignment; + bool bPrefetch; + bool bCanBeQueued; +}; + +//----------------------------------------------------------------------------- +// Purpose: Builds a cache of the data bytes for a specific .wav file +//----------------------------------------------------------------------------- +class CAsyncWaveData +{ +public: + explicit CAsyncWaveData(); + + // APIS required by CManagedDataCacheClient + void DestroyResource(); + CAsyncWaveData *GetData(); + unsigned int Size(); + + static void AsyncCallback( const FileAsyncRequest_t &asyncRequest, int numReadBytes, FSAsyncStatus_t err ); + static void QueuedLoaderCallback( void *pContext, void *pContext2, const void *pData, int nSize, LoaderError_t loaderError ); + static CAsyncWaveData *CreateResource( const asyncwaveparams_t ¶ms ); + static unsigned int EstimatedSize( const asyncwaveparams_t ¶ms ); + + void OnAsyncCompleted( const FileAsyncRequest_t* asyncFilePtr, int numReadBytes, FSAsyncStatus_t err ); + bool BlockingCopyData( void *destbuffer, int destbufsize, int startoffset, int count ); + bool BlockingGetDataPointer( void **ppData ); + void SetAsyncPriority( int priority ); + void StartAsyncLoading( const asyncwaveparams_t& params ); + bool GetPostProcessed(); + void SetPostProcessed( bool proc ); + + bool IsCurrentlyLoading(); + char const *GetFileName(); + + // Data +public: + int m_nDataSize; // bytes requested + int m_nReadSize; // bytes actually read + void *m_pvData; // target buffer + byte *m_pAlloc; // memory of buffer (base may not match) + FileAsyncRequest_t m_async; + FSAsyncControl_t m_hAsyncControl; + float m_start; // time at request invocation + float m_arrival; // time at data arrival + FileNameHandle_t m_hFileNameHandle; + int m_nBufferBytes; // size of any pre-allocated target buffer + BufferHandle_t m_hBuffer; // used to dequeue the buffer after lru + unsigned int m_bLoaded : 1; + unsigned int m_bMissing : 1; + unsigned int m_bPostProcessed : 1; +}; + +//----------------------------------------------------------------------------- +// Purpose: C'tor +//----------------------------------------------------------------------------- +CAsyncWaveData::CAsyncWaveData() : + m_nDataSize( 0 ), + m_nReadSize( 0 ), + m_pvData( 0 ), + m_pAlloc( 0 ), + m_hBuffer( INVALID_BUFFER_HANDLE ), + m_nBufferBytes( 0 ), + m_hAsyncControl( NULL ), + m_bLoaded( false ), + m_bMissing( false ), + m_start( 0.0 ), + m_arrival( 0.0 ), + m_bPostProcessed( false ), + m_hFileNameHandle( 0 ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: // APIS required by CDataLRU +//----------------------------------------------------------------------------- +void CAsyncWaveData::DestroyResource() +{ + if ( IsPC() ) + { + if ( m_hAsyncControl ) + { + if ( !m_bLoaded && !m_bMissing ) + { + // NOTE: We CANNOT call AsyncAbort since if the file is actually being read we'll end + // up still getting a callback, but our this ptr (deleted below) will be feeefeee and we'll trash the heap + // pretty bad. So we call AsyncFinish, which will do a blocking read and will definitely succeed + // Block until we are finished + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + } + + g_pFileSystem->AsyncRelease( m_hAsyncControl ); + m_hAsyncControl = NULL; + } + } + + if ( IsX360() ) + { + if ( m_hAsyncControl ) + { + if ( !m_bLoaded && !m_bMissing ) + { + // force an abort + int errStatus = g_pFileSystem->AsyncAbort( m_hAsyncControl ); + if ( errStatus != FSASYNC_ERR_UNKNOWNID ) + { + // must wait for abort to finish before deallocating data + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + } + } + g_pFileSystem->AsyncRelease( m_hAsyncControl ); + m_hAsyncControl = NULL; + } + if ( m_hBuffer != INVALID_BUFFER_HANDLE ) + { + // hint the manager that this tracked buffer is invalid + wavedatacache->MarkBufferDiscarded( m_hBuffer ); + } + } + + // delete buffers + if ( IsPC() || !IsX360() ) + { + g_pFileSystem->FreeOptimalReadBuffer( m_pAlloc ); + } + else + { + delete [] m_pAlloc; + } + + delete this; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +char const *CAsyncWaveData::GetFileName() +{ + static char sz[MAX_PATH]; + + if ( m_hFileNameHandle ) + { + if ( g_pFileSystem->String( m_hFileNameHandle, sz, sizeof( sz ) ) ) + { + return sz; + } + } + + Assert( 0 ); + return ""; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : CAsyncWaveData +//----------------------------------------------------------------------------- +CAsyncWaveData *CAsyncWaveData::GetData() +{ + return this; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : unsigned int +//----------------------------------------------------------------------------- +unsigned int CAsyncWaveData::Size() +{ + int size = sizeof( *this ); + + if ( IsPC() ) + { + size += m_nDataSize; + } + + if ( IsX360() ) + { + // the data size can shrink during streaming near end of file + // need the real contant size of this object's allocations + size += m_nBufferBytes; + } + + return size; +} + +//----------------------------------------------------------------------------- +// Purpose: Static method for CDataLRU +// Input : ¶ms - +// Output : CAsyncWaveData +//----------------------------------------------------------------------------- +CAsyncWaveData *CAsyncWaveData::CreateResource( const asyncwaveparams_t ¶ms ) +{ + CAsyncWaveData *pData = new CAsyncWaveData; + Assert( pData ); + if ( pData ) + { + if ( IsX360() ) + { + // create buffer now for re-use during streaming process + pData->m_nBufferBytes = AlignValue( params.datasize, params.alignment ); + pData->m_pAlloc = new byte[pData->m_nBufferBytes]; + pData->m_pvData = pData->m_pAlloc; + } + pData->StartAsyncLoading( params ); + } + + return pData; +} + +//----------------------------------------------------------------------------- +// Purpose: Static method +// Input : ¶ms - +// Output : static unsigned int +//----------------------------------------------------------------------------- +unsigned int CAsyncWaveData::EstimatedSize( const asyncwaveparams_t ¶ms ) +{ + int size = sizeof( CAsyncWaveData ); + + if ( IsPC() ) + { + size += params.datasize; + } + if ( IsX360() ) + { + // the expected size of this object's allocations + size += AlignValue( params.datasize, params.alignment ); + } + + return size; +} + +//----------------------------------------------------------------------------- +// Purpose: Static method, called by thread, don't call anything non-threadsafe from handler!!! +// Input : asyncFilePtr - +// numReadBytes - +// err - +//----------------------------------------------------------------------------- +void CAsyncWaveData::AsyncCallback(const FileAsyncRequest_t &asyncRequest, int numReadBytes, FSAsyncStatus_t err ) +{ + CAsyncWaveData *pObject = reinterpret_cast< CAsyncWaveData * >( asyncRequest.pContext ); + Assert( pObject ); + if ( pObject ) + { + pObject->OnAsyncCompleted( &asyncRequest, numReadBytes, err ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Static method, called by thread, don't call anything non-threadsafe from handler!!! +//----------------------------------------------------------------------------- +void CAsyncWaveData::QueuedLoaderCallback( void *pContext, void *pContext2, const void *pData, int nSize, LoaderError_t loaderError ) +{ + CAsyncWaveData *pObject = reinterpret_cast< CAsyncWaveData * >( pContext ); + Assert( pObject ); + + pObject->OnAsyncCompleted( NULL, nSize, loaderError == LOADERERROR_NONE ? FSASYNC_OK : FSASYNC_ERR_FILEOPEN ); +} + +//----------------------------------------------------------------------------- +// Purpose: NOTE: THIS IS CALLED FROM A THREAD SO YOU CAN'T CALL INTO ANYTHING NON-THREADSAFE +// such as CUtlSymbolTable/CUtlDict (many of the CUtl* are non-thread safe)!!! +// Input : asyncFilePtr - +// numReadBytes - +// err - +//----------------------------------------------------------------------------- +void CAsyncWaveData::OnAsyncCompleted( const FileAsyncRequest_t *asyncFilePtr, int numReadBytes, FSAsyncStatus_t err ) +{ + if ( IsPC() ) + { + // Take hold of pointer (we can just use delete[] across .dlls because we are using a shared memory allocator...) + if ( err == FSASYNC_OK || err == FSASYNC_ERR_READING ) + { + m_arrival = ( float )Plat_FloatTime(); + + // Take over ptr + m_pAlloc = ( byte * )asyncFilePtr->pData; + if ( SndAlignReads() ) + { + m_async.nOffset = ( m_async.nBytes - m_nDataSize ); + m_async.nBytes -= m_async.nOffset; + m_pvData = ((byte *)m_pAlloc) + m_async.nOffset; + m_nReadSize = numReadBytes - m_async.nOffset; + } + else + { + m_pvData = m_pAlloc; + m_nReadSize = numReadBytes; + } + + // Needs to be post-processed + m_bPostProcessed = false; + + // Finished loading + m_bLoaded = true; + } + else if ( err == FSASYNC_ERR_FILEOPEN ) + { + // SEE NOTE IN FUNCTION COMMENT ABOVE!!! + // Tracker 22905, et al. + // Because this api gets called from the other thread, don't spew warning here as it can + // cause a crash in searching CUtlSymbolTables since they use a global var for a LessFunc context!!! + m_bMissing = true; + } + } + + if ( IsX360() ) + { + m_arrival = (float)Plat_FloatTime(); + + // possibly reading more than intended due to alignment restriction + m_nReadSize = numReadBytes; + if ( m_nReadSize > m_nDataSize ) + { + // clamp to expected, extra data is unreliable + m_nReadSize = m_nDataSize; + } + + if ( err != FSASYNC_OK ) + { + // track as any error + m_bMissing = true; + } + + if ( err != FSASYNC_ERR_FILEOPEN ) + { + // some data got loaded + m_bLoaded = true; + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *destbuffer - +// destbufsize - +// startoffset - +// count - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWaveData::BlockingCopyData( void *destbuffer, int destbufsize, int startoffset, int count ) +{ + if ( !m_bLoaded ) + { + Assert( m_hAsyncControl ); + // Force it to finish + // It could finish between the above line and here, but the AsyncFinish call will just have a bogus id, not a big deal + if ( SndAsyncSpewBlocking() ) + { + // Force it to finish + float st = ( float )Plat_FloatTime(); + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + float ed = ( float )Plat_FloatTime(); + Warning( "%f BCD: Async I/O Force %s (%8.2f msec / %8.2f msec total)\n", realtime, GetFileName(), 1000.0f * (float)( ed - st ), 1000.0f * (float)( m_arrival - m_start ) ); + } + else + { + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + } + } + + // notify on any error + if ( m_bMissing ) + { + // Only warn once + m_bMissing = false; + + char fn[MAX_PATH]; + if ( g_pFileSystem->String( m_hFileNameHandle, fn, sizeof( fn ) ) ) + { + MaybeReportMissingWav( fn ); + } + } + + if ( !m_bLoaded ) + { + return false; + } + else if ( m_arrival != 0 && snd_async_spew.GetBool() ) + { + DevMsg( "%f Async I/O Read successful %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( m_arrival - m_start ) ); + m_arrival = 0; + } + + // clamp requested to available + if ( count > m_nReadSize ) + { + count = m_nReadSize - startoffset; + } + + if ( count < 0 ) + { + return false; + } + + // Copy data from stream buffer + Q_memcpy( destbuffer, (char *)m_pvData + ( startoffset - m_async.nOffset ), count ); + + g_pFileSystem->AsyncRelease( m_hAsyncControl ); + m_hAsyncControl = NULL; + return true; +} + + +bool CAsyncWaveData::IsCurrentlyLoading() +{ + if ( m_bLoaded ) + return true; + FSAsyncStatus_t status = g_pFileSystem->AsyncStatus( m_hAsyncControl ); + if ( status == FSASYNC_STATUS_INPROGRESS || status == FSASYNC_OK ) + return true; + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : **ppData - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWaveData::BlockingGetDataPointer( void **ppData ) +{ + Assert( ppData ); + if ( !m_bLoaded ) + { + // Force it to finish + // It could finish between the above line and here, but the AsyncFinish call will just have a bogus id, not a big deal + if ( SndAsyncSpewBlocking() ) + { + float st = ( float )Plat_FloatTime(); + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + float ed = ( float )Plat_FloatTime(); + Warning( "%f BlockingGetDataPointer: Async I/O Force %s (%8.2f msec / %8.2f msec total )\n", realtime, GetFileName(), 1000.0f * (float)( ed - st ), 1000.0f * (float)( m_arrival - m_start ) ); + } + else + { + g_pFileSystem->AsyncFinish( m_hAsyncControl, true ); + } + } + + // notify on any error + if ( m_bMissing ) + { + // Only warn once + m_bMissing = false; + + char fn[MAX_PATH]; + if ( g_pFileSystem->String( m_hFileNameHandle, fn, sizeof( fn ) ) ) + { + MaybeReportMissingWav( fn ); + } + } + + if ( !m_bLoaded ) + { + return false; + } + else if ( m_arrival != 0 && snd_async_spew.GetBool() ) + { + DevMsg( "%f Async I/O Read successful %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( m_arrival - m_start ) ); + m_arrival = 0; + } + + *ppData = m_pvData; + + g_pFileSystem->AsyncRelease( m_hAsyncControl ); + m_hAsyncControl = NULL; + + return true; +} + +void CAsyncWaveData::SetAsyncPriority( int priority ) +{ + if ( m_async.priority != priority ) + { + m_async.priority = priority; + g_pFileSystem->AsyncSetPriority( m_hAsyncControl, m_async.priority ); + if ( snd_async_spew.GetBool() ) + { + DevMsg( "%f Async I/O Bumped priority for %s (%8.2f msec)\n", realtime, GetFileName(), 1000.0f * (float)( Plat_FloatTime() - m_start ) ); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : params - +//----------------------------------------------------------------------------- +void CAsyncWaveData::StartAsyncLoading( const asyncwaveparams_t& params ) +{ + Assert( IsX360() || ( IsPC() && !m_bLoaded ) ); + + // expected to be relative to the sound\ dir + m_hFileNameHandle = params.hFilename; + + // build the real filename + char szFilename[MAX_PATH]; + Q_snprintf( szFilename, sizeof( szFilename ), "sound\\%s", GetFileName() ); + + int nPriority = 1; + if ( params.bPrefetch ) + { + // lower the priority of prefetched sounds, so they don't block immediate sounds from being loaded + nPriority = 0; + } + + if ( !IsX360() ) + { + m_async.pData = NULL; + if ( SndAlignReads() ) + { + m_async.nOffset = 0; + m_async.nBytes = params.seekpos + params.datasize; + } + else + { + m_async.nOffset = params.seekpos; + m_async.nBytes = params.datasize; + } + } + else + { + Assert( params.datasize > 0 ); + + // using explicit allocated buffer on xbox + m_async.pData = m_pvData; + m_async.nOffset = params.seekpos; + m_async.nBytes = AlignValue( params.datasize, params.alignment ); + } + + m_async.pfnCallback = AsyncCallback; // optional completion callback + m_async.pContext = (void *)this; // caller's unique context + m_async.priority = nPriority; // inter list priority, 0=lowest + m_async.flags = IsX360() ? 0 : FSASYNC_FLAGS_ALLOCNOFREE; + m_async.pszPathID = "GAME"; + + m_bLoaded = false; + m_bMissing = false; + m_nDataSize = params.datasize; + m_start = (float)Plat_FloatTime(); + m_arrival = 0; + m_nReadSize = 0; + m_bPostProcessed = false; + + // The async layer creates a copy of this string, ok to send a local reference + m_async.pszFilename = szFilename; + + char szFullName[MAX_PATH]; + if ( IsX360() && ( g_pFileSystem->GetDVDMode() == DVDMODE_STRICT ) ) + { + // all audio is expected be in zips + // resolve to absolute name now, where path can be filtered to just the zips (fast find, no real i/o) + // otherwise the dvd will do a costly seek for each zip miss due to search path fall through + PathTypeQuery_t pathType; + if ( !g_pFileSystem->RelativePathToFullPath( m_async.pszFilename, m_async.pszPathID, szFullName, sizeof( szFullName ), FILTER_CULLNONPACK, &pathType ) ) + { + // not found, do callback now to handle error + m_async.pfnCallback( m_async, 0, FSASYNC_ERR_FILEOPEN ); + return; + } + m_async.pszFilename = szFullName; + } + + if ( IsX360() && params.bCanBeQueued ) + { + // queued loader takes over + LoaderJob_t loaderJob; + loaderJob.m_pFilename = m_async.pszFilename; + loaderJob.m_pPathID = m_async.pszPathID; + loaderJob.m_pCallback = QueuedLoaderCallback; + loaderJob.m_pContext = (void *)this; + loaderJob.m_Priority = LOADERPRIORITY_DURINGPRELOAD; + loaderJob.m_pTargetData = m_async.pData; + loaderJob.m_nBytesToRead = m_async.nBytes; + loaderJob.m_nStartOffset = m_async.nOffset; + g_pQueuedLoader->AddJob( &loaderJob ); + return; + } + + MEM_ALLOC_CREDIT(); + + // Commence async I/O + Assert( !m_hAsyncControl ); + g_pFileSystem->AsyncRead( m_async, &m_hAsyncControl ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWaveData::GetPostProcessed() +{ + return m_bPostProcessed; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : proc - +//----------------------------------------------------------------------------- +void CAsyncWaveData::SetPostProcessed( bool proc ) +{ + m_bPostProcessed = proc; +} + +//----------------------------------------------------------------------------- +// Purpose: Implements a cache of .wav / .mp3 data based on filename +//----------------------------------------------------------------------------- +class CAsyncWavDataCache : public IAsyncWavDataCache, + public CManagedDataCacheClient<CAsyncWaveData, asyncwaveparams_t> +{ +public: + CAsyncWavDataCache(); + ~CAsyncWavDataCache() {} + + virtual bool Init( unsigned int memSize ); + virtual void Shutdown(); + + // implementation that treats file as monolithic + virtual memhandle_t AsyncLoadCache( char const *filename, int datasize, int startpos, bool bIsPrefetch = false ); + virtual void PrefetchCache( char const *filename, int datasize, int startpos ); + virtual bool CopyDataIntoMemory( char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ); + virtual bool CopyDataIntoMemory( memhandle_t& handle, char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ); + virtual void SetPostProcessed( memhandle_t handle, bool proc ); + virtual void Unload( memhandle_t handle ); + virtual bool GetDataPointer( memhandle_t& handle, char const *filename, int datasize, int startpos, void **pData, int copystartpos, bool *pbPostProcessed ); + virtual bool IsDataLoadCompleted( memhandle_t handle, bool *pIsValid ); + virtual void RestartDataLoad( memhandle_t* handle, char const *filename, int datasize, int startpos ); + virtual bool IsDataLoadInProgress( memhandle_t handle ); + + // Xbox: alternate multi-buffer streaming implementation + virtual StreamHandle_t OpenStreamedLoad( char const *pFileName, int dataSize, int dataStart, int startPos, int loopPos, int bufferSize, int numBuffers, streamFlags_t flags ); + virtual void CloseStreamedLoad( StreamHandle_t hStream ); + virtual int CopyStreamedDataIntoMemory( StreamHandle_t hStream, void *pBuffer, int bufferSize, int copyStartPos, int bytesToCopy ); + virtual bool IsStreamedDataReady( StreamHandle_t hStream ); + virtual void MarkBufferDiscarded( BufferHandle_t hBuffer ); + virtual void *GetStreamedDataPointer( StreamHandle_t hStream, bool bSync ); + + virtual void Flush(); + virtual void OnMixBegin(); + virtual void OnMixEnd(); + + void QueueUnlock( const memhandle_t &handle ); + void SpewMemoryUsage( int level ); + + // Cache helpers + bool GetItemName( DataCacheClientID_t clientId, const void *pItem, char *pDest, unsigned nMaxLen ); + +private: + void Clear(); + + struct CacheEntry_t + { + CacheEntry_t() : + name( 0 ), + handle( 0 ) + { + } + FileNameHandle_t name; + memhandle_t handle; + }; + + // tags the signature of a buffer inside a rb tree for faster than linear find + struct BufferEntry_t + { + FileNameHandle_t m_hName; + memhandle_t m_hWaveData; + int m_StartPos; + bool m_bCanBeShared; + }; + + static bool BufferHandleLessFunc( const BufferEntry_t& lhs, const BufferEntry_t& rhs ) + { + if ( lhs.m_hName != rhs.m_hName ) + { + return lhs.m_hName < rhs.m_hName; + } + + if ( lhs.m_StartPos != rhs.m_StartPos ) + { + return lhs.m_StartPos < rhs.m_StartPos; + } + + return lhs.m_bCanBeShared < rhs.m_bCanBeShared; + } + + CUtlRBTree< BufferEntry_t, BufferHandle_t > m_BufferList; + + // encapsulates (n) buffers for a streamed wave object + struct StreamedEntry_t + { + FileNameHandle_t m_hName; + memhandle_t m_hWaveData[STREAM_BUFFER_COUNT]; + int m_Front; // buffer index, forever incrementing + int m_NextStartPos; // predicted offset if mixing linearly + int m_DataSize; // length of the data set in bytes + int m_DataStart; // file offset where data set starts + int m_LoopStart; // offset in data set where loop starts + int m_BufferSize; // size of the buffer in bytes + int m_numBuffers; // number of buffers (1 or 2) to march through + int m_SectorSize; // size of sector on stream device + bool m_bSinglePlay; // hint to keep same buffers + }; + CUtlLinkedList< StreamedEntry_t, StreamHandle_t > m_StreamedHandles; + + static bool CacheHandleLessFunc( const CacheEntry_t& lhs, const CacheEntry_t& rhs ) + { + return lhs.name < rhs.name; + } + CUtlRBTree< CacheEntry_t, int > m_CacheHandles; + + memhandle_t FindOrCreateBuffer( asyncwaveparams_t ¶ms, bool bFind ); + bool m_bInitialized; + bool m_bQueueCacheUnlocks; + CUtlVector<memhandle_t> m_unlockQueue; +}; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CAsyncWavDataCache::CAsyncWavDataCache() : + m_CacheHandles( 0, 0, CacheHandleLessFunc ), + m_BufferList( 0, 0, BufferHandleLessFunc ), + m_bInitialized( false ), + m_bQueueCacheUnlocks( false ) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::Init( unsigned int memSize ) +{ + if ( m_bInitialized ) + return true; + + if ( IsX360() ) + { + const char *pGame = engineClient->GetGameDirectory(); + if ( !Q_stricmp( Q_UnqualifiedFileName( pGame ), "tf" ) ) + { + memSize = TF_XBOX_WAV_MEMORY_CACHE; + } + else + { + memSize = DEFAULT_XBOX_WAV_MEMORY_CACHE; + } + } + else + { + if ( memSize < DEFAULT_WAV_MEMORY_CACHE ) + { + memSize = DEFAULT_WAV_MEMORY_CACHE; + } + } + +#if FORCE_SMALL_MEMORY_CACHE_SIZE + memSize = FORCE_SMALL_MEMORY_CACHE_SIZE; + Msg( "WARNING CAsyncWavDataCache::Init() forcing small memory cache size: %u\n", memSize ); +#endif + + CCacheClientBaseClass::Init( g_pDataCache, "WaveData", memSize ); + + m_bInitialized = true; + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::Shutdown() +{ + if ( !m_bInitialized ) + { + return; + } + + Clear(); + + CCacheClientBaseClass::Shutdown(); + + m_bInitialized = false; +} + +//----------------------------------------------------------------------------- +// Purpose: Creates initial cache object if it doesn't already exist, starts async loading the actual data +// in any case. +// Input : *filename - +// datasize - +// startpos - +// Output : memhandle_t +//----------------------------------------------------------------------------- +memhandle_t CAsyncWavDataCache::AsyncLoadCache( char const *filename, int datasize, int startpos, bool bIsPrefetch ) +{ + VPROF( "CAsyncWavDataCache::AsyncLoadCache" ); + + FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename ); + + CacheEntry_t search; + search.name = fnh; + search.handle = 0; + + // find or create the handle + int idx = m_CacheHandles.Find( search ); + if ( idx == m_CacheHandles.InvalidIndex() ) + { + idx = m_CacheHandles.Insert( search ); + Assert( idx != m_CacheHandles.InvalidIndex() ); + } + + CacheEntry_t &entry = m_CacheHandles[idx]; + + // Try and pull it into cache + CAsyncWaveData *data = CacheGet( entry.handle ); + if ( !data ) + { + // Try and reload it + asyncwaveparams_t params; + params.hFilename = fnh; + params.datasize = datasize; + params.seekpos = startpos; + params.bPrefetch = bIsPrefetch; + entry.handle = CacheCreate( params ); + } + + return entry.handle; +} + + +//----------------------------------------------------------------------------- +// Purpose: Reclaim a buffer. A reclaimed resident buffer is ready for play. +//----------------------------------------------------------------------------- +memhandle_t CAsyncWavDataCache::FindOrCreateBuffer( asyncwaveparams_t ¶ms, bool bFind ) +{ + CAsyncWaveData *pWaveData; + BufferEntry_t search; + BufferHandle_t hBuffer; + + search.m_hName = params.hFilename; + search.m_StartPos = params.seekpos; + search.m_bCanBeShared = bFind; + search.m_hWaveData = 0; + + if ( bFind ) + { + // look for an existing buffer that matches exactly (same file, offset, and share) + int iBuffer = m_BufferList.Find( search ); + if ( iBuffer != m_BufferList.InvalidIndex() ) + { + // found + search.m_hWaveData = m_BufferList[iBuffer].m_hWaveData; + if ( snd_async_stream_spew.GetInt() >= 2 ) + { + char tempBuff[MAX_PATH]; + g_pFileSystem->String( params.hFilename, tempBuff, sizeof( tempBuff ) ); + Msg( "Found Buffer: %s, offset: %d\n", tempBuff, params.seekpos ); + } + } + } + + // each resource buffer stays locked (valid) while in use + // a buffering stream is not subject to lru and can rely on it's buffers + // a buffering stream may obsolete it's buffers by reducing the lock count, allowing for lru + pWaveData = CacheLock( search.m_hWaveData ); + if ( !pWaveData ) + { + // not in cache, create and lock + // not found, create buffer and fill with data + search.m_hWaveData = CacheCreate( params, DCAF_LOCK ); + + // add the buffer to our managed list + hBuffer = m_BufferList.Insert( search ); + Assert( hBuffer != m_BufferList.InvalidIndex() ); + + // store the handle into our managed list + // used during a lru discard as a means to keep the list in-sync + pWaveData = CacheGet( search.m_hWaveData ); + pWaveData->m_hBuffer = hBuffer; + } + else + { + // still in cache + // same as requesting it and having it arrive instantly + pWaveData->m_start = pWaveData->m_arrival = (float)Plat_FloatTime(); + } + + return search.m_hWaveData; +} + + +//----------------------------------------------------------------------------- +// Purpose: Load data asynchronously via multi-buffers, returns specialized handle +//----------------------------------------------------------------------------- +StreamHandle_t CAsyncWavDataCache::OpenStreamedLoad( char const *pFileName, int dataSize, int dataStart, int startPos, int loopPos, int bufferSize, int numBuffers, streamFlags_t flags ) +{ + VPROF( "CAsyncWavDataCache::OpenStreamedLoad" ); + + StreamedEntry_t streamedEntry; + StreamHandle_t hStream; + asyncwaveparams_t params; + int i; + + Assert( numBuffers > 0 && numBuffers <= STREAM_BUFFER_COUNT ); + + // queued load mandates one buffer + Assert( !( flags & STREAMED_QUEUEDLOAD ) || numBuffers == 1 ); + + streamedEntry.m_hName = g_pFileSystem->FindOrAddFileName( pFileName ); + streamedEntry.m_Front = 0; + streamedEntry.m_DataSize = dataSize; + streamedEntry.m_DataStart = dataStart; + streamedEntry.m_NextStartPos = startPos + numBuffers * bufferSize; + streamedEntry.m_LoopStart = loopPos; + streamedEntry.m_BufferSize = bufferSize; + streamedEntry.m_numBuffers = numBuffers; + streamedEntry.m_bSinglePlay = ( flags & STREAMED_SINGLEPLAY ) != 0; + streamedEntry.m_SectorSize = ( IsX360() && ( flags & STREAMED_FROMDVD ) ) ? XBOX_DVD_SECTORSIZE : 1; + + // single play streams expect to uniquely own and thus recycle their buffers though the data + // single play streams are guaranteed that their buffers are private and cannot be shared + // a non-single play stream wants persisting buffers and attempts to reclaim a matching buffer + bool bFindBuffer = ( streamedEntry.m_bSinglePlay == false ); + + // initial load populates buffers + // mixing starts after front buffer viable + // buffer rotation occurs after front buffer consumed + // there should be no blocking + params.hFilename = streamedEntry.m_hName; + params.datasize = bufferSize; + params.alignment = streamedEntry.m_SectorSize; + params.bCanBeQueued = ( flags & STREAMED_QUEUEDLOAD ) != 0; + for ( i=0; i<numBuffers; ++i ) + { + params.seekpos = dataStart + startPos + i * bufferSize; + streamedEntry.m_hWaveData[i] = FindOrCreateBuffer( params, bFindBuffer ); + } + + // get a unique handle for each stream request + hStream = m_StreamedHandles.AddToTail( streamedEntry ); + Assert( hStream != m_StreamedHandles.InvalidIndex() ); + + return hStream; +} + + +//----------------------------------------------------------------------------- +// Purpose: Cleanup a streamed load's resources. +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::CloseStreamedLoad( StreamHandle_t hStream ) +{ + VPROF( "CAsyncWavDataCache::CloseStreamedLoad" ); + + if ( hStream == INVALID_STREAM_HANDLE ) + { + return; + } + + int lockCount; + StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream]; + for ( int i=0; i<streamedEntry.m_numBuffers; ++i ) + { + // multiple streams could be using the same buffer, keeping the lock count nonzero + lockCount = GetCacheSection()->GetLockCount( streamedEntry.m_hWaveData[i] ); + Assert( lockCount >= 1 ); + if ( lockCount > 0 ) + { + lockCount = CacheUnlock( streamedEntry.m_hWaveData[i] ); + } + + if ( streamedEntry.m_bSinglePlay ) + { + // a buffering single play stream has no reason to reuse its own buffers and destroys them + Assert( lockCount == 0 ); + CacheRemove( streamedEntry.m_hWaveData[i] ); + } + } + + m_StreamedHandles.Remove( hStream ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *filename - +// datasize - +// startpos - +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::PrefetchCache( char const *filename, int datasize, int startpos ) +{ + // Just do an async load, but don't get cache handle + AsyncLoadCache( filename, datasize, startpos, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *filename - +// datasize - +// startpos - +// *buffer - +// bufsize - +// copystartpos - +// bytestocopy - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::CopyDataIntoMemory( char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ) +{ + VPROF( "CAsyncWavDataCache::CopyDataIntoMemory" ); + + bool bret = false; + + // Add to caching system + AsyncLoadCache( filename, datasize, startpos ); + + FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename ); + + CacheEntry_t search; + search.name = fnh; + search.handle = 0; + + // Now look it up, it should be in the system + int idx = m_CacheHandles.Find( search ); + if ( idx == m_CacheHandles.InvalidIndex() ) + { + Assert( 0 ); + return bret; + } + + // Now see if the handle has been paged out... + return CopyDataIntoMemory( m_CacheHandles[ idx ].handle, filename, datasize, startpos, buffer, bufsize, copystartpos, bytestocopy, pbPostProcessed ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : handle - +// *filename - +// datasize - +// startpos - +// *buffer - +// bufsize - +// copystartpos - +// bytestocopy - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::CopyDataIntoMemory( memhandle_t& handle, char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ) +{ + VPROF( "CAsyncWavDataCache::CopyDataIntoMemory" ); + + *pbPostProcessed = false; + + bool bret = false; + + CAsyncWaveData *data = CacheLock( handle ); + if ( !data ) + { + FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename ); + + CacheEntry_t search; + search.name = fnh; + search.handle = 0; + + // Now look it up, it should be in the system + int idx = m_CacheHandles.Find( search ); + if ( idx == m_CacheHandles.InvalidIndex() ) + { + Assert( 0 ); + return false; + } + + // Try and reload it + asyncwaveparams_t params; + params.hFilename = fnh; + params.datasize = datasize; + params.seekpos = startpos; + + handle = m_CacheHandles[ idx ].handle = CacheCreate( params ); + data = CacheLock( handle ); + if ( !data ) + { + return bret; + } + } + + // Cache entry exists, but if filesize == 0 then the file itself wasn't on disk... + if ( data->m_nDataSize != 0 ) + { + bret = data->BlockingCopyData( buffer, bufsize, copystartpos, bytestocopy ); + } + + *pbPostProcessed = data->GetPostProcessed(); + + // Release lock + CacheUnlock( handle ); + return bret; +} + + +//----------------------------------------------------------------------------- +// Purpose: Copy from streaming buffers into target memory, never blocks. +//----------------------------------------------------------------------------- +int CAsyncWavDataCache::CopyStreamedDataIntoMemory( int hStream, void *pBuffer, int bufferSize, int copyStartPos, int bytesToCopy ) +{ + VPROF( "CAsyncWavDataCache::CopyStreamedDataIntoMemory" ); + + int actualCopied; + int count; + int i; + int which; + CAsyncWaveData *pWaveData[STREAM_BUFFER_COUNT]; + CAsyncWaveData *pFront; + asyncwaveparams_t params; + int nextStartPos; + int bufferPos; + bool bEndOfFile; + int index; + bool bWaiting; + bool bCompleted; + StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream]; + + if ( copyStartPos >= streamedEntry.m_DataStart + streamedEntry.m_DataSize ) + { + // at or past end of file + return 0; + } + + for ( i=0; i<streamedEntry.m_numBuffers; ++i ) + { + pWaveData[i] = CacheGetNoTouch( streamedEntry.m_hWaveData[i] ); + Assert( pWaveData[i] ); + } + + // drive the buffering + index = streamedEntry.m_Front; + bEndOfFile = 0; + actualCopied = 0; + bWaiting = false; + while ( 1 ) + { + // try to satisfy from the front + pFront = pWaveData[index % streamedEntry.m_numBuffers]; + bufferPos = copyStartPos - pFront->m_async.nOffset; + + // cache atomic async completion signal off to avoid coherency issues + bCompleted = pFront->m_bLoaded || pFront->m_bMissing; + + if ( snd_async_stream_spew.GetInt() >= 1 ) + { + // interval is the audio block clock rate, the block must be available within this interval + // a faster audio rate or smaller block size implies a smaller interval + // latency is the actual block delivery time + // latency must not exceed the delivery interval or stariving occurs and audio pops + float nowTime = Plat_FloatTime(); + int interval = (int)(1000.0f*(nowTime-pFront->m_start)); + int latency; + if ( bCompleted && pFront->m_bLoaded ) + { + latency = (int)(1000.0f*(pFront->m_arrival-pFront->m_start)); + } + else + { + // buffer has not arrived yet + latency = -1; + } + DevMsg( "Stream:%2d interval:%5dms latency:%5dms offset:%d length:%d (%s)\n", hStream, interval, latency, pFront->m_async.nOffset, pFront->m_nReadSize, pFront->GetFileName() ); + } + + if ( bCompleted && pFront->m_hAsyncControl && ( pFront->m_bLoaded || pFront->m_bMissing) ) + { + g_pFileSystem->AsyncRelease( pFront->m_hAsyncControl ); + pFront->m_hAsyncControl = NULL; + } + + if ( bCompleted && pFront->m_bLoaded ) + { + if ( bufferPos >= 0 && bufferPos < pFront->m_nReadSize ) + { + count = bytesToCopy; + if ( bufferPos + bytesToCopy > pFront->m_nReadSize ) + { + // clamp requested to actual available + count = pFront->m_nReadSize - bufferPos; + } + if ( bufferPos + count > bufferSize ) + { + // clamp requested to caller's buffer dimension + count = bufferSize - bufferPos; + } + + Q_memcpy( pBuffer, (char *)pFront->m_pvData + bufferPos, count ); + + // advance past consumed bytes + actualCopied += count; + copyStartPos += count; + bufferPos += count; + } + } + else if ( bCompleted && pFront->m_bMissing ) + { + // notify on any error + MaybeReportMissingWav( pFront->GetFileName() ); + break; + } + else + { + // data not available + bWaiting = true; + break; + } + + // cycle past obsolete or consumed buffers + if ( bufferPos < 0 || bufferPos >= pFront->m_nReadSize ) + { + // move to next buffer + index++; + if ( index - streamedEntry.m_Front >= streamedEntry.m_numBuffers ) + { + // out of buffers + break; + } + } + + if ( actualCopied == bytesToCopy ) + { + // satisfied request + break; + } + } + + if ( streamedEntry.m_numBuffers > 1 ) + { + // restart consumed buffers + while ( streamedEntry.m_Front < index ) + { + if ( !actualCopied && !bWaiting ) + { + // couldn't return any data because the buffers aren't in the right location + // oh no! caller must be skipping + // due to latency the next buffer position has to start one full buffer ahead of the caller's desired read location + // hopefully only 1 buffer will stutter + nextStartPos = copyStartPos - streamedEntry.m_DataStart + streamedEntry.m_BufferSize; + + // advance past, ready for next possible iteration + copyStartPos += streamedEntry.m_BufferSize; + } + else + { + // get the next forecasted read location + nextStartPos = streamedEntry.m_NextStartPos; + } + + if ( nextStartPos >= streamedEntry.m_DataSize ) + { + // next buffer is at or past end of file + if ( streamedEntry.m_LoopStart >= 0 ) + { + // wrap back around to loop position + nextStartPos = streamedEntry.m_LoopStart; + } + else + { + // advance past consumed buffer + streamedEntry.m_Front++; + + // start no further buffers + break; + } + } + + // still valid data left to read + // snap the buffer position to required alignment + nextStartPos = streamedEntry.m_SectorSize * (nextStartPos/streamedEntry.m_SectorSize); + + // start loading back buffer at future location + params.hFilename = streamedEntry.m_hName; + params.seekpos = streamedEntry.m_DataStart + nextStartPos; + params.datasize = streamedEntry.m_DataSize - nextStartPos; + params.alignment = streamedEntry.m_SectorSize; + if ( params.datasize > streamedEntry.m_BufferSize ) + { + // clamp to buffer size + params.datasize = streamedEntry.m_BufferSize; + } + + // save next start position + streamedEntry.m_NextStartPos = nextStartPos + params.datasize; + + which = streamedEntry.m_Front % streamedEntry.m_numBuffers; + if ( streamedEntry.m_bSinglePlay ) + { + // a single play wave has no reason to persist its buffers into the lru + // reuse buffer and restart until finished + pWaveData[which]->StartAsyncLoading( params ); + } + else + { + // release obsolete buffer to lru management + CacheUnlock( streamedEntry.m_hWaveData[which] ); + // reclaim or create/load the desired buffer + streamedEntry.m_hWaveData[which] = FindOrCreateBuffer( params, true ); + } + + streamedEntry.m_Front++; + } + + if ( bWaiting ) + { + // oh no! data needed is not yet available in front buffer + // caller requesting data faster than can be provided or caller skipped + // can only return what has been copied thus far (could be 0) + return actualCopied; + } + } + + return actualCopied; +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the front buffer, optionally block. +// Intended for user of a single buffer stream. +//----------------------------------------------------------------------------- +void *CAsyncWavDataCache::GetStreamedDataPointer( StreamHandle_t hStream, bool bSync ) +{ + void *pData; + CAsyncWaveData *pFront; + int index; + StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream]; + + index = streamedEntry.m_Front % streamedEntry.m_numBuffers; + pFront = CacheGetNoTouch( streamedEntry.m_hWaveData[index] ); + Assert( pFront ); + if ( !pFront ) + { + // shouldn't happen + return NULL; + } + + if ( !pFront->m_bMissing && pFront->m_bLoaded ) + { + return pFront->m_pvData; + } + + if ( bSync && pFront->BlockingGetDataPointer( &pData ) ) + { + return pData; + } + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: The front buffer must be valid +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::IsStreamedDataReady( int hStream ) +{ + VPROF( "CAsyncWavDataCache::IsStreamedDataReady" ); + + if ( hStream == INVALID_STREAM_HANDLE ) + { + return false; + } + + StreamedEntry_t &streamedEntry = m_StreamedHandles[hStream]; + + if ( streamedEntry.m_Front ) + { + // already streaming, the buffers better be arriving as expected + return true; + } + + // only the first front buffer must be present + CAsyncWaveData *pFront = CacheGetNoTouch( streamedEntry.m_hWaveData[0] ); + Assert( pFront ); + if ( !pFront ) + { + // shouldn't happen + // let the caller think data is ready, so stream can shutdown + return true; + } + + // regardless of any errors + // errors handled during data fetch + return pFront->m_bLoaded || pFront->m_bMissing; +} + + +//----------------------------------------------------------------------------- +// Purpose: Dequeue the buffer entry (backdoor for list management) +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::MarkBufferDiscarded( BufferHandle_t hBuffer ) +{ + m_BufferList.RemoveAt( hBuffer ); +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : handle - +// proc - +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::SetPostProcessed( memhandle_t handle, bool proc ) +{ + CAsyncWaveData *data = CacheGet( handle ); + if ( data ) + { + data->SetPostProcessed( proc ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : handle - +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::Unload( memhandle_t handle ) +{ + // Don't actually unload, just mark it as stale + if ( GetCacheSection() ) + { + GetCacheSection()->Age( handle ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : handle - +// *filename - +// datasize - +// startpos - +// **pData - +// copystartpos - +// *pbPostProcessed - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::GetDataPointer( memhandle_t& handle, char const *filename, int datasize, int startpos, void **pData, int copystartpos, bool *pbPostProcessed ) +{ + VPROF( "CAsyncWavDataCache::GetDataPointer" ); + + Assert( pbPostProcessed ); + Assert( pData ); + + *pbPostProcessed = false; + + bool bret = false; + *pData = NULL; + + CAsyncWaveData *data = CacheLock( handle ); + if ( !data ) + { + FileNameHandle_t fnh = g_pFileSystem->FindOrAddFileName( filename ); + + CacheEntry_t search; + search.name = fnh; + search.handle = 0; + + int idx = m_CacheHandles.Find( search ); + if ( idx == m_CacheHandles.InvalidIndex() ) + { + Assert( 0 ); + return bret; + } + + // Try and reload it + asyncwaveparams_t params; + params.hFilename = fnh; + params.datasize = datasize; + params.seekpos = startpos; + + handle = m_CacheHandles[ idx ].handle = CacheCreate( params ); + data = CacheLock( handle ); + if ( !data ) + { + return bret; + } + } + + // Cache entry exists, but if filesize == 0 then the file itself wasn't on disk... + if ( data->m_nDataSize != 0 ) + { + if ( datasize != data->m_nDataSize ) + { + // We've had issues where we are called with datasize larger than what we read on disk. + // Ie: datasize is 277,180, data->m_nDataSize is 263,168 + // This can happen due to a corrupted audio cache, but it's more likely that somehow + // we wound up reading the cache data from one language and the file from another. + DevMsg( "Cached datasize != sound datasize %d - %d.\n", datasize, data->m_nDataSize ); +#ifdef STAGING_ONLY + // Adding a STAGING_ONLY debugger break to try and help track this down. Hopefully we'll + // get this crash internally with full debug information instead of just minidump files. + DebuggerBreak(); +#endif + } + else if ( copystartpos < data->m_nDataSize ) + { + if ( data->BlockingGetDataPointer( pData ) ) + { + *pData = (char *)*pData + copystartpos; + bret = true; + } + } + } + + *pbPostProcessed = data->GetPostProcessed(); + + // Release lock at the end of mixing + QueueUnlock( handle ); + return bret; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : handle - +// *filename - +// datasize - +// startpos - +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::IsDataLoadCompleted( memhandle_t handle, bool *pIsValid ) +{ + VPROF( "CAsyncWavDataCache::IsDataLoadCompleted" ); + + CAsyncWaveData *data = CacheGet( handle ); + if ( !data ) + { + *pIsValid = false; + return false; + } + *pIsValid = true; + // bump the priority + data->SetAsyncPriority( 1 ); + + return data->m_bLoaded; +} + + +void CAsyncWavDataCache::RestartDataLoad( memhandle_t *pHandle, const char *pFilename, int dataSize, int startpos ) +{ + CAsyncWaveData *data = CacheGet( *pHandle ); + if ( !data ) + { + *pHandle = AsyncLoadCache( pFilename, dataSize, startpos ); + } +} + +bool CAsyncWavDataCache::IsDataLoadInProgress( memhandle_t handle ) +{ + CAsyncWaveData *data = CacheGet( handle ); + if ( data ) + { + return data->IsCurrentlyLoading(); + } + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::Flush() +{ + GetCacheSection()->Flush(); + SpewMemoryUsage( 0 ); +} + +void CAsyncWavDataCache::QueueUnlock( const memhandle_t &handle ) +{ + // not queuing right now, just unlock + if ( !m_bQueueCacheUnlocks ) + { + CacheUnlock( handle ); + return; + } + // queue to unlock at the end of mixing + m_unlockQueue.AddToTail( handle ); +} + +void CAsyncWavDataCache::OnMixBegin() +{ + Assert( !m_bQueueCacheUnlocks ); + m_bQueueCacheUnlocks = true; + Assert( m_unlockQueue.Count() == 0 ); +} + +void CAsyncWavDataCache::OnMixEnd() +{ + m_bQueueCacheUnlocks = false; + // flush the unlock queue + for ( int i = 0; i < m_unlockQueue.Count(); i++ ) + { + CacheUnlock( m_unlockQueue[i] ); + } + m_unlockQueue.RemoveAll(); +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +bool CAsyncWavDataCache::GetItemName( DataCacheClientID_t clientId, const void *pItem, char *pDest, unsigned nMaxLen ) +{ + CAsyncWaveData *pWaveData = (CAsyncWaveData *)pItem; + Q_strncpy( pDest, pWaveData->GetFileName(), nMaxLen ); + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Spew a cache summary to the console +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::SpewMemoryUsage( int level ) +{ + DataCacheStatus_t status; + DataCacheLimits_t limits; + GetCacheSection()->GetStatus( &status, &limits ); + int bytesUsed = status.nBytes; + int bytesTotal = limits.nMaxBytes; + + if ( IsPC() ) + { + float percent = 100.0f * (float)bytesUsed / (float)bytesTotal; + + Msg( "CAsyncWavDataCache: %i .wavs total %s, %.2f %% of capacity\n", m_CacheHandles.Count(), Q_pretifymem( bytesUsed, 2 ), percent ); + + if ( level >= 1 ) + { + for ( int i = m_CacheHandles.FirstInorder(); m_CacheHandles.IsValidIndex(i); i = m_CacheHandles.NextInorder(i) ) + { + char name[MAX_PATH]; + if ( !g_pFileSystem->String( m_CacheHandles[ i ].name, name, sizeof( name ) ) ) + { + Assert( 0 ); + continue; + } + memhandle_t &handle = m_CacheHandles[ i ].handle; + CAsyncWaveData *data = CacheGetNoTouch( handle ); + if ( data ) + { + Msg( "\t%16.16s : %s\n", Q_pretifymem(data->Size()),name); + } + else + { + Msg( "\t%16.16s : %s\n", "not resident",name); + } + } + Msg( "CAsyncWavDataCache: %i .wavs total %s, %.2f %% of capacity\n", m_CacheHandles.Count(), Q_pretifymem( bytesUsed, 2 ), percent ); + } + } + + if ( IsX360() ) + { + CAsyncWaveData *pData; + BufferEntry_t *pBuffer; + BufferHandle_t h; + float percent; + int lockCount; + + if ( bytesTotal <= 0 ) + { + // unbounded, indeterminate + percent = 0; + bytesTotal = 0; + } + else + { + percent = 100.0f*(float)bytesUsed/(float)bytesTotal; + } + + if ( level >= 1 ) + { + // detail buffers + ConMsg( "Streaming Buffer List:\n" ); + for ( h = m_BufferList.FirstInorder(); h != m_BufferList.InvalidIndex(); h = m_BufferList.NextInorder( h ) ) + { + pBuffer = &m_BufferList[h]; + pData = CacheGetNoTouch( pBuffer->m_hWaveData ); + lockCount = GetCacheSection()->GetLockCount( pBuffer->m_hWaveData ); + + CacheLockMutex(); + if ( pData ) + { + ConMsg( "Start:%7d Length:%7d Lock:%3d %s\n", pData->m_async.nOffset, pData->m_nDataSize, lockCount, pData->GetFileName() ); + } + CacheUnlockMutex(); + } + } + + ConMsg( "CAsyncWavDataCache: %.2f MB used of %.2f MB, %.2f%% of capacity", (float)bytesUsed/(1024.0f*1024.0f), (float)bytesTotal/(1024.0f*1024.0f), percent ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAsyncWavDataCache::Clear() +{ + for ( int i = m_CacheHandles.FirstInorder(); m_CacheHandles.IsValidIndex(i); i = m_CacheHandles.NextInorder(i) ) + { + CacheEntry_t& dat = m_CacheHandles[i]; + CacheRemove( dat.handle ); + } + m_CacheHandles.RemoveAll(); + + FOR_EACH_LL( m_StreamedHandles, i ) + { + StreamedEntry_t &dat = m_StreamedHandles[i]; + for ( int j=0; j<dat.m_numBuffers; ++j ) + { + GetCacheSection()->BreakLock( dat.m_hWaveData[j] ); + CacheRemove( dat.m_hWaveData[j] ); + } + } + m_StreamedHandles.RemoveAll(); + m_BufferList.RemoveAll(); +} + + +static CAsyncWavDataCache g_AsyncWaveDataCache; +IAsyncWavDataCache *wavedatacache = &g_AsyncWaveDataCache; + +CON_COMMAND( snd_async_flush, "Flush all unlocked async audio data" ) +{ + g_AsyncWaveDataCache.Flush(); +} + +CON_COMMAND( snd_async_showmem, "Show async memory stats" ) +{ + g_AsyncWaveDataCache.SpewMemoryUsage( 1 ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *pFileName - +// dataOffset - +// dataSize - +//----------------------------------------------------------------------------- +void PrefetchDataStream( const char *pFileName, int dataOffset, int dataSize ) +{ + if ( IsX360() ) + { + // Xbox streaming buffer implementation does not support this "hinting" + return; + } + + wavedatacache->PrefetchCache( pFileName, dataSize, dataOffset ); +} + +//----------------------------------------------------------------------------- +// Purpose: This is an instance of a stream. +// This contains the file handle and streaming buffer +// The mixer doesn't know the file is streaming. The IWaveData +// abstracts the data access. The mixer abstracts data encoding/format +//----------------------------------------------------------------------------- +class CWaveDataStreamAsync : public IWaveData +{ +public: + CWaveDataStreamAsync( CAudioSource &source, IWaveStreamSource *pStreamSource, const char *pFileName, int fileStart, int fileSize, CSfxTable *sfx, int startOffset ); + ~CWaveDataStreamAsync( void ); + + // return the source pointer (mixer needs this to determine some things like sampling rate) + CAudioSource &Source( void ) { return m_source; } + + // Read data from the source - this is the primary function of a IWaveData subclass + // Get the data from the buffer (or reload from disk) + virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + bool IsValid() { return m_bValid; } + virtual bool IsReadyToMix(); + +private: + CWaveDataStreamAsync( const CWaveDataStreamAsync & ); + + //----------------------------------------------------------------------------- + // Purpose: + // Output : byte + //----------------------------------------------------------------------------- + inline byte *GetCachedDataPointer() + { + VPROF( "CWaveDataStreamAsync::GetCachedDataPointer" ); + + CAudioSourceCachedInfo *info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + if ( !info ) + { + Assert( !"CAudioSourceWave::GetCachedDataPointer info == NULL" ); + return NULL; + } + + return (byte *)info->CachedData(); + } + + char const *GetFileName(); + CAudioSource &m_source; // wave source + IWaveStreamSource *m_pStreamSource; // streaming + int m_sampleSize; // size of a sample in bytes + int m_waveSize; // total number of samples in the file + + int m_bufferSize; // size of buffer in samples + char *m_buffer; + int m_sampleIndex; + int m_bufferCount; + int m_dataStart; + int m_dataSize; + + memhandle_t m_hCache; + StreamHandle_t m_hStream; + FileNameHandle_t m_hFileName; + + bool m_bValid; + CAudioSourceCachedInfoHandle_t m_AudioCacheHandle; + int m_nCachedDataSize; + CSfxTable *m_pSfx; +}; + +CWaveDataStreamAsync::CWaveDataStreamAsync + ( + CAudioSource &source, + IWaveStreamSource *pStreamSource, + const char *pFileName, + int fileStart, + int fileSize, + CSfxTable *sfx, + int startOffset + ) : + m_source( source ), + m_dataStart( fileStart ), + m_dataSize( fileSize ), + m_pStreamSource( pStreamSource ), + m_bValid( false ), + m_hCache( 0 ), + m_hStream( INVALID_STREAM_HANDLE ), + m_hFileName( 0 ), + m_pSfx( sfx ) +{ + m_hFileName = g_pFileSystem->FindOrAddFileName( pFileName ); + + // nothing in the buffer yet + m_sampleIndex = 0; + m_bufferCount = 0; + + if ( IsPC() ) + { + m_buffer = new char[SINGLE_BUFFER_SIZE]; + Q_memset( m_buffer, 0, SINGLE_BUFFER_SIZE ); + } + + m_nCachedDataSize = 0; + + if ( m_dataSize <= 0 ) + { + DevMsg(1, "Can't find streaming wav file: sound\\%s\n", GetFileName() ); + return; + } + + if ( IsPC() ) + { + m_hCache = wavedatacache->AsyncLoadCache( GetFileName(), m_dataSize, m_dataStart ); + + // size of a sample + m_sampleSize = source.SampleSize(); + // size in samples of the buffer + m_bufferSize = SINGLE_BUFFER_SIZE / m_sampleSize; + // size in samples (not bytes) of the wave itself + m_waveSize = fileSize / m_sampleSize; + + m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + } + + if ( IsX360() ) + { + // size of a sample + m_sampleSize = source.SampleSize(); + // size in samples (not bytes) of the wave itself + m_waveSize = fileSize / m_sampleSize; + + streamFlags_t flags = STREAMED_FROMDVD; + + if ( !Q_strnicmp( pFileName, "music", 5 ) && ( pFileName[5] == '\\' || pFileName[5] == '/') ) + { + // music discards and cycles its buffers + flags |= STREAMED_SINGLEPLAY; + } + else if ( !Q_strnicmp( pFileName, "vo", 2 ) && ( pFileName[2] == '\\' || pFileName[2] == '/' ) && !source.IsSentenceWord() ) + { + // vo discards and cycles its buffers, except for sentence sources, which do recur + flags |= STREAMED_SINGLEPLAY; + } + + int bufferSize; + if ( source.Format() == WAVE_FORMAT_XMA ) + { + // each xma block has its own compression rate + // the buffer must be large enough to cover worst case delivery i/o latency + // the xma mixer expects quantum xma blocks + COMPILE_TIME_ASSERT( ( STREAM_BUFFER_DATASIZE % XMA_BLOCK_SIZE ) == 0 ); + bufferSize = STREAM_BUFFER_DATASIZE; + } + else + { + // calculate a worst case buffer size based on rate + bufferSize = STREAM_BUFFER_TIME*source.SampleRate()*m_sampleSize; + if ( source.Format() == WAVE_FORMAT_ADPCM ) + { + // consider adpcm as 4 bit samples + bufferSize /= 2; + } + + if ( source.IsLooped() ) + { + // lighten the streaming load for looping samples + // doubling the buffer halves the buffer search/load requests + bufferSize *= 2; + } + } + + // streaming buffers obey alignments + bufferSize = AlignValue( bufferSize, XBOX_DVD_SECTORSIZE ); + + // use double buffering + int numBuffers = 2; + + if ( m_dataSize <= STREAM_BUFFER_DATASIZE || m_dataSize <= numBuffers*bufferSize ) + { + // no gain for buffering a small file or multiple buffering + // match the expected transfer with a single buffer + bufferSize = m_dataSize; + numBuffers = 1; + } + + // size in samples of the transfer buffer + m_bufferSize = bufferSize / m_sampleSize; + + // allocate a transfer buffer + // matches the size of the streaming buffer exactly + // ensures that buffers can be filled and then consumed/requeued at the same time + m_buffer = new char[bufferSize]; + + int loopStart; + if ( source.IsLooped() ) + { + int loopBlock; + loopStart = m_pStreamSource->GetLoopingInfo( &loopBlock, NULL, NULL ) * m_sampleSize; + if ( source.Format() == WAVE_FORMAT_XMA ) + { + // xma works in blocks, mixer handles inter-block accurate loop positioning + // block streaming will cycle from the block where the loop occurs + loopStart = loopBlock * XMA_BLOCK_SIZE; + } + } + else + { + // sample not looped + loopStart = -1; + } + + // load the file piecewise through a buffering implementation + m_hStream = wavedatacache->OpenStreamedLoad( pFileName, m_dataSize, m_dataStart, startOffset, loopStart, bufferSize, numBuffers, flags ); + } + + m_bValid = true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CWaveDataStreamAsync::~CWaveDataStreamAsync( void ) +{ + if ( IsPC() && m_source.IsPlayOnce() && m_source.CanDelete() ) + { + m_source.SetPlayOnce( false ); // in case it gets used again + wavedatacache->Unload( m_hCache ); + } + + if ( IsX360() ) + { + wavedatacache->CloseStreamedLoad( m_hStream ); + } + + delete [] m_buffer; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +char const *CWaveDataStreamAsync::GetFileName() +{ + static char fn[MAX_PATH]; + + if ( m_hFileName ) + { + if ( g_pFileSystem->String( m_hFileName, fn, sizeof( fn ) ) ) + { + return fn; + } + } + + Assert( 0 ); + return ""; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWaveDataStreamAsync::IsReadyToMix() +{ + if ( IsPC() ) + { + // If not async loaded, start mixing right away + if ( !m_source.IsAsyncLoad() && !snd_async_fullyasync.GetBool() ) + { + return true; + } + + bool bCacheValid; + bool bLoaded = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ); + if ( !bCacheValid ) + { + wavedatacache->RestartDataLoad( &m_hCache, GetFileName(), m_dataSize, m_dataStart ); + } + return bLoaded; + } + + if ( IsX360() ) + { + return wavedatacache->IsStreamedDataReady( m_hStream ); + } + + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Read data from the source - this is the primary function of a IWaveData subclass +// Get the data from the buffer (or reload from disk) +// Input : **pData - +// sampleIndex - +// sampleCount - +// copyBuf[AUDIOSOURCE_COPYBUF_SIZE] - +// Output : int +//----------------------------------------------------------------------------- +int CWaveDataStreamAsync::ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + // Current file position + int seekpos = m_dataStart + m_sampleIndex * m_sampleSize; + + // wrap position if looping + if ( m_source.IsLooped() ) + { + sampleIndex = m_pStreamSource->UpdateLoopingSamplePosition( sampleIndex ); + if ( sampleIndex < m_sampleIndex ) + { + // looped back, buffer has no samples yet + m_sampleIndex = sampleIndex; + m_bufferCount = 0; + + // update file position + seekpos = m_dataStart + sampleIndex * m_sampleSize; + } + } + + // UNDONE: This is an error!! + // The mixer playing back the stream tried to go backwards!?!?! + // BUGBUG: Just play the beginning of the buffer until we get to a valid linear position + if ( sampleIndex < m_sampleIndex ) + sampleIndex = m_sampleIndex; + + // calc sample position relative to the current buffer + // m_sampleIndex is the sample position of the first byte of the buffer + sampleIndex -= m_sampleIndex; + + // out of range? refresh buffer + if ( sampleIndex >= m_bufferCount ) + { + // advance one buffer (the file is positioned here) + m_sampleIndex += m_bufferCount; + // next sample to load + sampleIndex -= m_bufferCount; + + // if the remainder is greated than one buffer size, seek over it. Otherwise, read the next chunk + // and leave the remainder as an offset. + + // number of buffers to "skip" (as in the case where we are starting a streaming sound not at the beginning) + int skips = sampleIndex / m_bufferSize; + + // If we are skipping over a buffer, do it with a seek instead of a read. + if ( skips ) + { + // skip directly to next position + m_sampleIndex += sampleIndex; + sampleIndex = 0; + } + + // move the file to the new position + seekpos = m_dataStart + (m_sampleIndex * m_sampleSize); + + // This is the maximum number of samples we could read from the file + m_bufferCount = m_waveSize - m_sampleIndex; + + // past the end of the file? stop the wave. + if ( m_bufferCount <= 0 ) + return 0; + + // clamp available samples to buffer size + if ( m_bufferCount > m_bufferSize ) + m_bufferCount = m_bufferSize; + + if ( IsPC() ) + { + // See if we can load in the intial data right out of the cached data lump instead. + int cacheddatastartpos = ( seekpos - m_dataStart ); + + // FastGet doesn't call into IsPrecachedSound if the handle appears valid... + CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet(); + if ( !info ) + { + // Full recache + info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + } + + bool startupCacheUsed = false; + + if ( info && + ( m_nCachedDataSize > 0 ) && + ( cacheddatastartpos < m_nCachedDataSize ) ) + { + // Get a ptr to the cached data + const byte *cacheddata = info->CachedData(); + if ( cacheddata ) + { + // See how many samples of cached data are available (cacheddatastartpos is zero on the first read) + int availSamples = ( m_nCachedDataSize - cacheddatastartpos ) / m_sampleSize; + + // Clamp to size of our internal buffer + if ( availSamples > m_bufferSize ) + { + availSamples = m_bufferSize; + } + + // Mark how many we are returning + m_bufferCount = availSamples; + // Copy raw sample data directly out of cache + Q_memcpy( m_buffer, ( char * )cacheddata + cacheddatastartpos, availSamples * m_sampleSize ); + + startupCacheUsed = true; + } + } + + // Not in startup cache, grab data from async cache loader (will block if data hasn't arrived yet) + if ( !startupCacheUsed ) + { + bool postprocessed = false; + + // read in the max bufferable, available samples + if ( !wavedatacache->CopyDataIntoMemory( + m_hCache, + GetFileName(), + m_dataSize, + m_dataStart, + m_buffer, + sizeof( m_buffer ), + seekpos, + m_bufferCount * m_sampleSize, + &postprocessed ) ) + { + return 0; + } + + // do any conversion the source needs (mixer will decode/decompress) + if ( !postprocessed ) + { + // Note that we don't set the postprocessed flag on the underlying data, since for streaming we're copying the + // original data into this buffer instead. + m_pStreamSource->UpdateSamples( m_buffer, m_bufferCount ); + } + } + } + + if ( IsX360() ) + { + if ( m_hStream != INVALID_STREAM_HANDLE ) + { + // request available data, may get less + // drives the buffering + m_bufferCount = wavedatacache->CopyStreamedDataIntoMemory( + m_hStream, + m_buffer, + m_bufferSize * m_sampleSize, + seekpos, + m_bufferCount * m_sampleSize ); + // convert to number of samples in the buffer + m_bufferCount /= m_sampleSize; + } + else + { + return 0; + } + + // do any conversion now the source needs (mixer will decode/decompress) on this buffer + m_pStreamSource->UpdateSamples( m_buffer, m_bufferCount ); + } + } + + // If we have some samples in the buffer that are within range of the request + // Use unsigned comparisons so that if sampleIndex is somehow negative that + // will be treated as out of range. + if ( (unsigned)sampleIndex < (unsigned)m_bufferCount ) + { + // Get the desired starting sample + *pData = (void *)&m_buffer[sampleIndex * m_sampleSize]; + + // max available + int available = m_bufferCount - sampleIndex; + // clamp available to max requested + if ( available > sampleCount ) + available = sampleCount; + + return available; + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Iterator for wave data (this is to abstract streaming/buffering) +//----------------------------------------------------------------------------- +class CWaveDataMemoryAsync : public IWaveData +{ +public: + CWaveDataMemoryAsync( CAudioSource &source ); + ~CWaveDataMemoryAsync( void ) {} + CAudioSource &Source( void ) { return m_source; } + + virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + virtual bool IsReadyToMix(); + +private: + CAudioSource &m_source; // pointer to source +}; + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &source - +//----------------------------------------------------------------------------- +CWaveDataMemoryAsync::CWaveDataMemoryAsync( CAudioSource &source ) : + m_source(source) +{ +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : **pData - +// sampleIndex - +// sampleCount - +// copyBuf[AUDIOSOURCE_COPYBUF_SIZE] - +// Output : int +//----------------------------------------------------------------------------- +int CWaveDataMemoryAsync::ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + return m_source.GetOutputData( pData, sampleIndex, sampleCount, copyBuf ); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CWaveDataMemoryAsync::IsReadyToMix() +{ + if ( !m_source.IsAsyncLoad() && !snd_async_fullyasync.GetBool() ) + { + // Wait until we're pending at least + if ( m_source.GetCacheStatus() == CAudioSource::AUDIO_NOT_LOADED ) + { + return false; + } + return true; + } + + if ( m_source.IsCached() ) + { + return true; + } + + if ( IsPC() ) + { + // Msg( "Waiting for data '%s'\n", m_source.GetFileName() ); + m_source.CacheLoad(); + } + + if ( IsX360() ) + { + // expected to be resident and valid, otherwise being called prior to load + Assert( 0 ); + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &source - +// *pStreamSource - +// &io - +// *pFileName - +// dataOffset - +// dataSize - +// Output : IWaveData +//----------------------------------------------------------------------------- +IWaveData *CreateWaveDataStream( CAudioSource &source, IWaveStreamSource *pStreamSource, const char *pFileName, int dataStart, int dataSize, CSfxTable *pSfx, int startOffset ) +{ + CWaveDataStreamAsync *pStream = new CWaveDataStreamAsync( source, pStreamSource, pFileName, dataStart, dataSize, pSfx, startOffset ); + if ( !pStream || !pStream->IsValid() ) + { + delete pStream; + pStream = NULL; + } + return pStream; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &source - +// Output : IWaveData +//----------------------------------------------------------------------------- +IWaveData *CreateWaveDataMemory( CAudioSource &source ) +{ + CWaveDataMemoryAsync *mem = new CWaveDataMemoryAsync( source ); + return mem; +} diff --git a/engine/audio/private/snd_wave_data.h b/engine/audio/private/snd_wave_data.h new file mode 100644 index 0000000..54a8285 --- /dev/null +++ b/engine/audio/private/snd_wave_data.h @@ -0,0 +1,45 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_WAVE_DATA_H +#define SND_WAVE_DATA_H +#ifdef _WIN32 +#pragma once +#endif + +#include "snd_audio_source.h" + +//----------------------------------------------------------------------------- +// Purpose: Linear iterator over source data. +// Keeps track of position in source, and maintains necessary buffers +//----------------------------------------------------------------------------- +abstract_class IWaveData +{ +public: + virtual ~IWaveData( void ) {} + virtual CAudioSource &Source( void ) = 0; + virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) = 0; + virtual bool IsReadyToMix() = 0; +}; + +abstract_class IWaveStreamSource +{ +public: + virtual int UpdateLoopingSamplePosition( int samplePosition ) = 0; + virtual void UpdateSamples( char *pData, int sampleCount ) = 0; + virtual int GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples ) = 0; +}; + +class IFileReadBinary; +class CSfxTable; + +extern IWaveData *CreateWaveDataStream( CAudioSource &source, IWaveStreamSource *pStreamSource, const char *pFileName, int dataStart, int dataSize, CSfxTable *pSfx, int startOffset ); +extern IWaveData *CreateWaveDataMemory( CAudioSource &source ); + +void PrefetchDataStream( const char *pFileName, int dataOffset, int dataSize ); + +#endif // SND_WAVE_DATA_H diff --git a/engine/audio/private/snd_wave_mixer.cpp b/engine/audio/private/snd_wave_mixer.cpp new file mode 100644 index 0000000..484b431 --- /dev/null +++ b/engine/audio/private/snd_wave_mixer.cpp @@ -0,0 +1,788 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#include "audio_pch.h" +#include "fmtstr.h" +#include "sysexternal.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern bool FUseHighQualityPitch( channel_t *pChannel ); + +//----------------------------------------------------------------------------- +// These mixers provide an abstraction layer between the audio device and +// mixing/decoding code. They allow data to be decoded and mixed using +// optimized, format sensitive code by calling back into the device that +// controls them. +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Purpose: maps mixing to 8-bit mono mixer +//----------------------------------------------------------------------------- +class CAudioMixerWave8Mono : public CAudioMixerWave +{ +public: + CAudioMixerWave8Mono( IWaveData *data ) : CAudioMixerWave( data ) {} + virtual int GetMixSampleSize() { return CalcSampleSize(8, 1); } + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) + { + pDevice->Mix8Mono( pChannel, (char *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: maps mixing to 8-bit stereo mixer +//----------------------------------------------------------------------------- +class CAudioMixerWave8Stereo : public CAudioMixerWave +{ +public: + CAudioMixerWave8Stereo( IWaveData *data ) : CAudioMixerWave( data ) {} + virtual int GetMixSampleSize( ) { return CalcSampleSize(8, 2); } + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) + { + pDevice->Mix8Stereo( pChannel, (char *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } +}; + +//----------------------------------------------------------------------------- +// Purpose: maps mixing to 16-bit mono mixer +//----------------------------------------------------------------------------- +class CAudioMixerWave16Mono : public CAudioMixerWave +{ +public: + CAudioMixerWave16Mono( IWaveData *data ) : CAudioMixerWave( data ) {} + virtual int GetMixSampleSize() { return CalcSampleSize(16, 1); } + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) + { + pDevice->Mix16Mono( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } +}; + + +//----------------------------------------------------------------------------- +// Purpose: maps mixing to 16-bit stereo mixer +//----------------------------------------------------------------------------- +class CAudioMixerWave16Stereo : public CAudioMixerWave +{ +public: + CAudioMixerWave16Stereo( IWaveData *data ) : CAudioMixerWave( data ) {} + virtual int GetMixSampleSize() { return CalcSampleSize(16, 2); } + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) + { + pDevice->Mix16Stereo( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } +}; + + +//----------------------------------------------------------------------------- +// Purpose: Create an appropriate mixer type given the data format +// Input : *data - data access abstraction +// format - pcm or adpcm (1 or 2 -- RIFF format) +// channels - number of audio channels (1 = mono, 2 = stereo) +// bits - bits per sample +// Output : CAudioMixer * abstract mixer type that maps mixing to appropriate code +//----------------------------------------------------------------------------- +CAudioMixer *CreateWaveMixer( IWaveData *data, int format, int nChannels, int bits, int initialStreamPosition ) +{ + CAudioMixer *pMixer = NULL; + + if ( format == WAVE_FORMAT_PCM ) + { + if ( nChannels > 1 ) + { + if ( bits == 8 ) + pMixer = new CAudioMixerWave8Stereo( data ); + else + pMixer = new CAudioMixerWave16Stereo( data ); + } + else + { + if ( bits == 8 ) + pMixer = new CAudioMixerWave8Mono( data ); + else + pMixer = new CAudioMixerWave16Mono( data ); + } + } + else if ( format == WAVE_FORMAT_ADPCM ) + { + return CreateADPCMMixer( data ); + } +#if defined( _X360 ) + else if ( format == WAVE_FORMAT_XMA ) + { + return CreateXMAMixer( data, initialStreamPosition ); + } +#endif + else + { + // unsupported format or wav file missing!!! + return NULL; + } + + if ( pMixer ) + { + Assert( CalcSampleSize(bits, nChannels ) == pMixer->GetMixSampleSize() ); + } + else + { + Assert( 0 ); + } + return pMixer; +} + + +//----------------------------------------------------------------------------- +// Purpose: Init the base WAVE mixer. +// Input : *data - data access object +//----------------------------------------------------------------------------- +CAudioMixerWave::CAudioMixerWave( IWaveData *data ) : m_pData(data) +{ + CAudioSource *pSource = GetSource(); + if ( pSource ) + { + pSource->ReferenceAdd( this ); + } + + m_fsample_index = 0; + m_sample_max_loaded = 0; + m_sample_loaded_index = -1; + m_finished = false; + m_forcedEndSample = 0; + m_delaySamples = 0; +} + + +//----------------------------------------------------------------------------- +// Purpose: Frees the data access object (we own it after construction) +//----------------------------------------------------------------------------- +CAudioMixerWave::~CAudioMixerWave( void ) +{ + CAudioSource *pSource = GetSource(); + if ( pSource ) + { + pSource->ReferenceRemove( this ); + } + delete m_pData; +} + +bool CAudioMixerWave::IsReadyToMix() +{ + return m_pData->IsReadyToMix(); +} + +//----------------------------------------------------------------------------- +// Purpose: Decode and read the data +// by default we just pass the request on to the data access object +// other mixers may need to buffer or decode the data for some reason +// +// Input : **pData - dest pointer +// sampleCount - number of samples needed +// Output : number of samples available in this batch +//----------------------------------------------------------------------------- +int CAudioMixerWave::GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + int samples_loaded; + // clear this out in case the underlying code leaves it unmodified + *pData = NULL; + samples_loaded = m_pData->ReadSourceData( pData, m_sample_max_loaded, sampleCount, copyBuf ); + + // keep track of total samples loaded + m_sample_max_loaded += samples_loaded; + + // keep track of index of last sample loaded + m_sample_loaded_index += samples_loaded; + + return samples_loaded; +} + + +//----------------------------------------------------------------------------- +// Purpose: calls through the wavedata to get the audio source +// Output : CAudioSource +//----------------------------------------------------------------------------- +CAudioSource *CAudioMixerWave::GetSource( void ) +{ + if ( m_pData ) + return &m_pData->Source(); + + return NULL; +} + + +//----------------------------------------------------------------------------- +// Purpose: Gets the current sample location in playback (index of next sample +// to be loaded). +// Output : int (samples from start of wave) +//----------------------------------------------------------------------------- +int CAudioMixerWave::GetSamplePosition( void ) +{ + return m_sample_max_loaded; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : delaySamples - +//----------------------------------------------------------------------------- +void CAudioMixerWave::SetStartupDelaySamples( int delaySamples ) +{ + m_delaySamples = delaySamples; +} + +// Move the current position to newPosition +void CAudioMixerWave::SetSampleStart( int newPosition ) +{ + CAudioSource *pSource = GetSource(); + if ( pSource ) + newPosition = pSource->ZeroCrossingAfter( newPosition ); + + m_fsample_index = newPosition; + + // index of last sample loaded - set to sample at new position + m_sample_loaded_index = newPosition; + m_sample_max_loaded = m_sample_loaded_index + 1; +} + +// End playback at newEndPosition +void CAudioMixerWave::SetSampleEnd( int newEndPosition ) +{ + // forced end of zero means play the whole sample + if ( !newEndPosition ) + newEndPosition = 1; + + CAudioSource *pSource = GetSource(); + if ( pSource ) + newEndPosition = pSource->ZeroCrossingBefore( newEndPosition ); + + // past current position? limit. + if ( newEndPosition < m_fsample_index ) + newEndPosition = m_fsample_index; + + m_forcedEndSample = newEndPosition; +} + +//----------------------------------------------------------------------------- +// Purpose: Skip source data (read but don't mix). The mixer must provide the +// full amount of samples or have silence in its output stream. +//----------------------------------------------------------------------------- +int CAudioMixerWave::SkipSamples( channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) +{ + float flTempPitch = pChannel->pitch; + pChannel->pitch = 1.0f; + int nRetVal = MixDataToDevice_( NULL, pChannel, sampleCount, outputRate, outputOffset, true ); + pChannel->pitch = flTempPitch; + return nRetVal; +} + +// wrapper routine to append without overflowing the temp buffer +static uint AppendToBuffer( char *pBuffer, const char *pSampleData, size_t nBytes, const char *pBufferEnd ) +{ +#if defined(_WIN32) && !defined(_X360) + // FIXME: Some clients are crashing here. Let's try to detect why. + if ( nBytes > 0 && ( (size_t)pBuffer <= 0xFFF || (size_t)pSampleData <= 0xFFF ) ) + { + Warning( "AppendToBuffer received potentially bad values (%p, %p, %u, %p)\n", pBuffer, pSampleData, (int)nBytes, pBufferEnd ); + } +#endif + + if ( pBufferEnd > pBuffer ) + { + size_t nAvail = pBufferEnd - pBuffer; + size_t nCopy = MIN( nBytes, nAvail ); + Q_memcpy( pBuffer, pSampleData, nCopy ); + return nCopy; + } + else + { + return 0; + } +} + +// Load a static copy buffer (g_temppaintbuffer) with the requested number of samples, +// with the first sample(s) in the buffer always set up as the last sample(s) of the previous load. +// Return a pointer to the head of the copy buffer. +// This ensures that interpolating pitch shifters always have the previous sample to reference. +// pChannel: sound's channel data +// sample_load_request: number of samples to load from source data +// pSamplesLoaded: returns the actual number of samples loaded (should always = sample_load_request) +// copyBuf: req'd by GetOutputData, used by some Mixers +// Returns: NULL ptr to data if no samples available, otherwise always fills remainder of copy buffer with +// 0 to pad remainder. +// NOTE: DO NOT MODIFY THIS ROUTINE (KELLYB) +char *CAudioMixerWave::LoadMixBuffer( channel_t *pChannel, int sample_load_request, int *pSamplesLoaded, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + int samples_loaded; + char *pSample = NULL; + char *pData = NULL; + int cCopySamps = 0; + + // save index of last sample loaded (updated in GetOutputData) + int sample_loaded_index = m_sample_loaded_index; + + // get data from source (copyBuf is expected to be available for use) + samples_loaded = GetOutputData( (void **)&pData, sample_load_request, copyBuf ); + if ( !samples_loaded && sample_load_request ) + { + // none available, bail out + // 360 might not be able to get samples due to latency of loop seek + // could also be the valid EOF for non-loops (caller keeps polling for data, until no more) + AssertOnce( IsX360() || !m_pData->Source().IsLooped() ); + *pSamplesLoaded = 0; + return NULL; + } + + int samplesize = GetMixSampleSize(); + const int nTempCopyBufferSize = ( TEMP_COPY_BUFFER_SIZE * sizeof( portable_samplepair_t ) ); + char *pCopy = (char *)g_temppaintbuffer; + const char *pCopyBufferEnd = pCopy + nTempCopyBufferSize; + + + + if ( IsX360() || IsDebug() ) + { + // for safety, 360 always validates sample request, due to new xma audio code and possible logic flaws + // PC can expect number of requested samples to be within tolerances due to exisiting aged code + // otherwise buffer overruns cause hard to track random crashes + if ( ( ( sample_load_request + 1 ) * samplesize ) > nTempCopyBufferSize ) + { + // make sure requested samples will fit in temp buffer. + // if this assert fails, then pitch is too high (ie: > 2.0) or the sample counters have diverged. + // NOTE: to prevent this, pitch should always be capped in MixDataToDevice (but isn't nor are the sample counters). + DevWarning( "LoadMixBuffer: sample load request %d exceeds buffer sizes\n", sample_load_request ); + Assert( 0 ); + *pSamplesLoaded = 0; + return NULL; + } + } + + // copy all samples from pData to copy buffer, set 0th sample to saved previous sample - this ensures + // interpolation pitch shift routines always have a previous sample to reference. + + // copy previous sample(s) to head of copy buffer pCopy + // In some cases, we'll need the previous 2 samples. This occurs when + // Rate < 1.0 - in example below, sample 4.86 - 6.48 requires samples 4-7 (previous samples saved are 4 & 5) + + /* + Example: + rate = 0.81, sampleCount = 3 (ie: # of samples to return ) + + _____load 3______ ____load 3_______ __load 2__ + + 0 1 2 3 4 5 6 7 sample_index (whole samples) + + ^ ^ ^ ^ ^ ^ ^ ^ ^ + | | | | | | | | | + 0 0.81 1.68 2.43 3.24 4.05 4.86 5.67 6.48 m_fsample_index (rate*sample) + _______________ ________________ ________________ + ^ ^ ^ ^ + | | | | + m_sample_loaded_index | | m_sample_loaded_index + | | + m_fsample_index---- ----m_fsample_index + + [return 3 samp] [return 3 samp] [return 3 samp] + */ + pSample = &(pChannel->sample_prev[0]); + + // determine how many saved samples we need to copy to head of copy buffer (0,1 or 2) + // so that pitch interpolation will correctly reference samples. + // NOTE: pitch interpolators always reference the sample before and after the indexed sample. + + // cCopySamps = sample_max_loaded - floor(m_fsample_index); + + if ( sample_loaded_index < 0 || (floor(m_fsample_index) > sample_loaded_index)) + { + // no samples previously loaded, or + // next sample index is entirely within the next block of samples to be loaded, + // so we won't need any samples from the previous block. (can occur when rate > 2.0) + cCopySamps = 0; + } + else if ( m_fsample_index < sample_loaded_index ) + { + // next sample index is entirely within the previous block of samples loaded, + // so we'll need the last 2 samples loaded. (can occur when rate < 1.0) + Assert ( ceil(m_fsample_index + 0.00000001) == sample_loaded_index ); + cCopySamps = 2; + } + else + { + // next sample index is between the next block and the previously loaded block, + // so we'll need the last sample loaded. (can occur when 1.0 < rate < 2.0) + Assert( floor(m_fsample_index) == sample_loaded_index ); + cCopySamps = 1; + } + Assert( cCopySamps >= 0 && cCopySamps <= 2 ); + + // point to the sample(s) we are to copy + if ( cCopySamps ) + { + pSample = cCopySamps == 1 ? pSample + samplesize : pSample; + pCopy += AppendToBuffer( pCopy, pSample, samplesize * cCopySamps, pCopyBufferEnd ); + } + + // copy loaded samples from pData into pCopy + // and update pointer to free space in copy buffer + if ( ( samples_loaded * samplesize ) != 0 && !pData ) + { + char const *pWavName = ""; + CSfxTable *source = pChannel->sfx; + if ( source ) + { + pWavName = source->getname(); + } + + Warning( "CAudioMixerWave::LoadMixBuffer: '%s' samples_loaded * samplesize = %i but pData == NULL\n", pWavName, ( samples_loaded * samplesize ) ); + *pSamplesLoaded = 0; + return NULL; + } + + pCopy += AppendToBuffer( pCopy, pData, samples_loaded * samplesize, pCopyBufferEnd ); + + // if we loaded fewer samples than we wanted to, and we're not + // delaying, load more samples or, if we run out of samples from non-looping source, + // pad copy buffer. + if ( samples_loaded < sample_load_request ) + { + // retry loading source data until 0 bytes returned, or we've loaded enough data. + // if we hit 0 bytes, fill remaining space in copy buffer with 0 and exit + int samples_load_extra; + int samples_loaded_retry = -1; + + for ( int k = 0; (k < 10000 && samples_loaded_retry && samples_loaded < sample_load_request); k++ ) + { + // how many more samples do we need to satisfy load request + samples_load_extra = sample_load_request - samples_loaded; + samples_loaded_retry = GetOutputData( (void**)&pData, samples_load_extra, copyBuf ); + + // copy loaded samples from pData into pCopy + if ( samples_loaded_retry ) + { + if ( ( samples_loaded_retry * samplesize ) != 0 && !pData ) + { + Warning( "CAudioMixerWave::LoadMixBuffer: samples_loaded_retry * samplesize = %i but pData == NULL\n", ( samples_loaded_retry * samplesize ) ); + *pSamplesLoaded = 0; + return NULL; + } + + pCopy += AppendToBuffer( pCopy, pData, samples_loaded_retry * samplesize, pCopyBufferEnd ); + samples_loaded += samples_loaded_retry; + } + } + } + + // if we still couldn't load the requested samples, fill rest of copy buffer with 0 + if ( samples_loaded < sample_load_request ) + { + // should always be able to get as many samples as we request from looping sound sources + AssertOnce ( IsX360() || !m_pData->Source().IsLooped() ); + + // these samples are filled with 0, not loaded. + // non-looping source hit end of data, fill rest of g_temppaintbuffer with 0 + int samples_zero_fill = sample_load_request - samples_loaded; + + int nAvail = pCopyBufferEnd - pCopy; + int nFill = samples_zero_fill * samplesize; + nFill = MIN( nAvail, nFill ); + Q_memset( pCopy, 0, nFill ); + pCopy += nFill; + samples_loaded += samples_zero_fill; + } + + if ( samples_loaded >= 2 ) + { + // always save last 2 samples from copy buffer to channel + // (we'll need 0,1 or 2 samples as start of next buffer for interpolation) + Assert( sizeof( pChannel->sample_prev ) >= samplesize*2 ); + pSample = pCopy - samplesize*2; + Q_memcpy( &(pChannel->sample_prev[0]), pSample, samplesize*2 ); + } + + // this routine must always return as many samples loaded (or zeros) as requested. + Assert( samples_loaded == sample_load_request ); + + *pSamplesLoaded = samples_loaded; + + return (char *)g_temppaintbuffer; +} + +// Helper routine to round (rate * samples) down to fixed point precision + +double RoundToFixedPoint( double rate, int samples, bool bInterpolated_pitch ) +{ + fixedint fixp_rate; + int64 d64_newSamps; // need to use double precision int to avoid overflow + + double newSamps; + + // get rate, in fixed point, determine new samples at rate + + if ( bInterpolated_pitch ) + fixp_rate = FIX_FLOAT14(rate); // 14 bit iterator + else + fixp_rate = FIX_FLOAT(rate); // 28 bit iterator + + // get number of new samples, convert back to float + + d64_newSamps = (int64)fixp_rate * (int64)samples; + + if ( bInterpolated_pitch ) + newSamps = FIX_14TODOUBLE(d64_newSamps); + else + newSamps = FIX_TODOUBLE(d64_newSamps); + + return newSamps; +} + +extern double MIX_GetMaxRate( double rate, int sampleCount ); + +// Helper routine for MixDataToDevice: +// Compute number of new samples to load at 'rate' so we can +// output 'sampleCount' samples, from m_fsample_index to fsample_index_end (inclusive) +// rate: sample rate +// sampleCountOut: number of samples calling routine needs to output +// bInterpolated_pitch: true if mixers use interpolating pitch shifters +int CAudioMixerWave::GetSampleLoadRequest( double rate, int sampleCountOut, bool bInterpolated_pitch ) +{ + double fsample_index_end; // index of last sample we'll need + int sample_index_high; // rounded up last sample index + int sample_load_request; // number of samples to load + + // NOTE: we must use fixed point math here, identical to math in mixers, to make sure + // we predict iteration results exactly. + // get floating point sample index of last sample we'll need + fsample_index_end = m_fsample_index + RoundToFixedPoint( rate, sampleCountOut-1, bInterpolated_pitch ); + + // always round up to ensure we'll have that n+1 sample for interpolation + sample_index_high = (int)( ceil( fsample_index_end ) ); + + // make sure we always round the floating point index up by at least 1 sample, + // ie: make sure integer sample_index_high is greater than floating point sample index + if ( (double)sample_index_high <= fsample_index_end ) + { + sample_index_high++; + } + Assert ( sample_index_high > fsample_index_end ); + + // attempt to load enough samples so we can reach sample_index_high sample. + sample_load_request = sample_index_high - m_sample_loaded_index; + Assert( sample_index_high >= m_sample_loaded_index ); + + // NOTE: we can actually return 0 samples to load if rate < 1.0 + // and sampleCountOut == 1. In this case, the output sample + // is computed from the previously saved buffer data. + return sample_load_request; +} + +int CAudioMixerWave::MixDataToDevice( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) +{ + return MixDataToDevice_(pDevice, pChannel, sampleCount, outputRate, outputOffset, false ); +} + +//----------------------------------------------------------------------------- +// Purpose: The device calls this to request data. The mixer must provide the +// full amount of samples or have silence in its output stream. +// Mix channel to all active paintbuffers. +// NOTE: cannot be called consecutively to mix into multiple paintbuffers! +// Input : *pDevice - requesting device +// sampleCount - number of samples at the output rate - should never be more than size of paintbuffer. +// outputRate - sampling rate of the request +// outputOffset - starting offset to mix to in paintbuffer +// bskipallmixing - true if we just want to skip ahead in source data + +// Output : Returns true to keep mixing, false to delete this mixer + +// NOTE: DO NOT MODIFY THIS ROUTINE (KELLYB) + +//----------------------------------------------------------------------------- +int CAudioMixerWave::MixDataToDevice_( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset, bool bSkipAllMixing ) +{ + // shouldn't be playing this if finished, but return if we are + if ( m_finished ) + return 0; + + tmZone( TELEMETRY_LEVEL0, TMZF_NONE, "%s", __FUNCTION__ ); + + // save this to compute total output + int startingOffset = outputOffset; + + double inputRate = (pChannel->pitch * m_pData->Source().SampleRate()); + double rate_max = inputRate / outputRate; + + // If we are terminating this wave prematurely, then make sure we detect the limit + if ( m_forcedEndSample ) + { + // How many total input samples will we need? + int samplesRequired = (int)(sampleCount * rate_max); + // will this hit the end? + if ( m_fsample_index + samplesRequired >= m_forcedEndSample ) + { + // yes, mark finished and truncate the sample request + m_finished = true; + sampleCount = (int)( (m_forcedEndSample - m_fsample_index) / rate_max ); + } + } + + /* + Example: + rate = 1.2, sampleCount = 3 (ie: # of samples to return ) + + ______load 4 samples_____ ________load 4 samples____ ___load 3 samples__ + + 0 1 2 3 4 5 6 7 8 9 10 sample_index (whole samples) + + ^ ^ ^ ^ ^ ^ ^ ^ ^ + | | | | | | | | | + 0 1.2 2.4 3.6 4.8 6.0 7.2 8.4 9.6 m_fsample_index (rate*sample) + _______return 3_______ _______return 3_______ _______return 3__________ + ^ ^ + | | + m_sample_loaded_index----- | (after first load 4 samples, this is where pointers are) + m_fsample_index--------- + */ + while ( sampleCount > 0 ) + { + bool advanceSample = true; + int samples_loaded, outputSampleCount; + char *pData = NULL; + double fsample_index_prev = m_fsample_index; // save so we can modify in LoadMixBuffer + bool bInterpolated_pitch = FUseHighQualityPitch( pChannel ); + double rate; + + VPROF_( bInterpolated_pitch ? "CAudioMixerWave::MixData innerloop interpolated" : "CAudioMixerWave::MixData innerloop not interpolated", 2, VPROF_BUDGETGROUP_OTHER_SOUND, false, BUDGETFLAG_OTHER ); + + // process samples in paintbuffer-sized batches + int sampleCountOut = min( sampleCount, PAINTBUFFER_SIZE ); + + // cap rate so that we never overflow the input copy buffer. + rate = MIX_GetMaxRate( rate_max, sampleCountOut ); + + if ( m_delaySamples > 0 ) + { + // If we are preceding sample playback with a delay, + // just fill data buffer with 0 value samples. + // Because there is no pitch shift applied, outputSampleCount == sampleCountOut. + int num_zero_samples = min( m_delaySamples, sampleCountOut ); + + // Decrement delay counter + m_delaySamples -= num_zero_samples; + + int sampleSize = GetMixSampleSize(); + int readBytes = sampleSize * num_zero_samples; + + // make sure we don't overflow temp copy buffer (g_temppaintbuffer) + Assert ( (TEMP_COPY_BUFFER_SIZE * sizeof(portable_samplepair_t)) > readBytes ); + pData = (char *)g_temppaintbuffer; + + // Now copy in some zeroes + memset( pData, 0, readBytes ); + + // we don't pitch shift these samples, so outputSampleCount == samples_loaded + samples_loaded = num_zero_samples; + outputSampleCount = num_zero_samples; + + advanceSample = false; + + // the zero samples are at the output rate, so set the input/output ratio to 1.0 + rate = 1.0f; + } + else + { + // ask the source for the data... + // temp buffer req'd by some data loaders + char copyBuf[AUDIOSOURCE_COPYBUF_SIZE]; + + // compute number of new samples to load at 'rate' so we can + // output 'sampleCount' samples, from m_fsample_index to fsample_index_end (inclusive) + int sample_load_request = GetSampleLoadRequest( rate, sampleCountOut, bInterpolated_pitch ); + + // return pointer to a new copy buffer (g_temppaintbuffer) loaded with sample_load_request samples + + // first sample(s), which are always the last sample(s) from the previous load. + // Always returns sample_load_request samples. Updates m_sample_max_loaded, m_sample_loaded_index. + pData = LoadMixBuffer( pChannel, sample_load_request, &samples_loaded, copyBuf ); + + // LoadMixBuffer should always return requested samples. + Assert ( !pData || ( samples_loaded == sample_load_request ) ); + + outputSampleCount = sampleCountOut; + } + + // no samples available + if ( !pData ) + { + break; + } + + // get sample fraction from 0th sample in copy buffer + double sampleFraction = m_fsample_index - floor( m_fsample_index ); + + // if just skipping samples in source, don't mix, just keep reading + if ( !bSkipAllMixing ) + { + // mix this data to all active paintbuffers + // Verify that we won't get a buffer overrun. + Assert( floor( sampleFraction + RoundToFixedPoint(rate, (outputSampleCount-1), bInterpolated_pitch) ) <= samples_loaded ); + + int saveIndex = MIX_GetCurrentPaintbufferIndex(); + for ( int i = 0 ; i < g_paintBuffers.Count(); i++ ) + { + if ( g_paintBuffers[i].factive ) + { + // mix channel into all active paintbuffers + MIX_SetCurrentPaintbuffer( i ); + + Mix( + pDevice, // Device. + pChannel, // Channel. + pData, // Input buffer. + outputOffset, // Output position. + FIX_FLOAT( sampleFraction ), // Iterators. + FIX_FLOAT( rate ), + outputSampleCount, + 0 ); + } + } + MIX_SetCurrentPaintbuffer( saveIndex ); + } + + if ( advanceSample ) + { + // update sample index to point to the next sample to output + // if we're not delaying + // Use fixed point math to make sure we exactly match results of mix + // iterators. + m_fsample_index = fsample_index_prev + RoundToFixedPoint( rate, outputSampleCount, bInterpolated_pitch ); + } + + outputOffset += outputSampleCount; + sampleCount -= outputSampleCount; + } + + // Did we run out of samples? if so, mark finished + if ( sampleCount > 0 ) + { + m_finished = true; + } + + // total number of samples mixed !!! at the output clock rate !!! + return outputOffset - startingOffset; +} + + +bool CAudioMixerWave::ShouldContinueMixing( void ) +{ + return !m_finished; +} + +float CAudioMixerWave::ModifyPitch( float pitch ) +{ + return pitch; +} + +float CAudioMixerWave::GetVolumeScale( void ) +{ + return 1.0f; +} + diff --git a/engine/audio/private/snd_wave_mixer.h b/engine/audio/private/snd_wave_mixer.h new file mode 100644 index 0000000..595fc7b --- /dev/null +++ b/engine/audio/private/snd_wave_mixer.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_WAVE_MIXER_H +#define SND_WAVE_MIXER_H +#pragma once + +class IWaveData; +class CAudioMixer; + +CAudioMixer *CreateWaveMixer( IWaveData *data, int format, int channels, int bits, int initialStreamPosition ); + + +#endif // SND_WAVE_MIXER_H diff --git a/engine/audio/private/snd_wave_mixer_adpcm.cpp b/engine/audio/private/snd_wave_mixer_adpcm.cpp new file mode 100644 index 0000000..ffa71f6 --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_adpcm.cpp @@ -0,0 +1,469 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#include "audio_pch.h" +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// max size of ADPCM block in bytes +#define MAX_BLOCK_SIZE 4096 + + +//----------------------------------------------------------------------------- +// Purpose: Mixer for ADPCM encoded audio +//----------------------------------------------------------------------------- +class CAudioMixerWaveADPCM : public CAudioMixerWave +{ +public: + CAudioMixerWaveADPCM( IWaveData *data ); + ~CAudioMixerWaveADPCM( void ); + + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ); + virtual int GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + + // need to override this to fixup blocks + void SetSampleStart( int newPosition ); + virtual int GetMixSampleSize() { return CalcSampleSize( 16, NumChannels() ); } + +private: + bool DecodeBlock( void ); + int NumChannels( void ); + void DecompressBlockMono( short *pOut, const char *pIn, int count ); + void DecompressBlockStereo( short *pOut, const char *pIn, int count ); + + const ADPCMWAVEFORMAT *m_pFormat; + const ADPCMCOEFSET *m_pCoefficients; + + short *m_pSamples; + int m_sampleCount; + int m_samplePosition; + + int m_blockSize; + int m_offset; + + int m_totalBytes; +}; + + +CAudioMixerWaveADPCM::CAudioMixerWaveADPCM( IWaveData *data ) : CAudioMixerWave( data ) +{ + m_pSamples = NULL; + m_sampleCount = 0; + m_samplePosition = 0; + m_offset = 0; + + CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source()); + +#ifdef _DEBUG + CAudioSource *pSource = NULL; + pSource = &m_pData->Source(); + Assert( dynamic_cast<CAudioSourceWave *>(pSource) != NULL ); +#endif + + m_pFormat = (const ADPCMWAVEFORMAT *)source.GetHeader(); + if ( m_pFormat ) + { + m_pCoefficients = (ADPCMCOEFSET *)((char *)m_pFormat + sizeof(WAVEFORMATEX) + 4); + + // create the decode buffer + m_pSamples = new short[m_pFormat->wSamplesPerBlock * m_pFormat->wfx.nChannels]; + + // number of bytes for samples + m_blockSize = ((m_pFormat->wSamplesPerBlock - 2) * m_pFormat->wfx.nChannels ) / 2; + // size of channel header + m_blockSize += 7 * m_pFormat->wfx.nChannels; + Assert( m_blockSize < MAX_BLOCK_SIZE ); + + m_totalBytes = source.DataSize(); + } +} + + +CAudioMixerWaveADPCM::~CAudioMixerWaveADPCM( void ) +{ + delete[] m_pSamples; +} + + +int CAudioMixerWaveADPCM::NumChannels( void ) +{ + if ( m_pFormat ) + { + return m_pFormat->wfx.nChannels; + } + return 0; +} + +void CAudioMixerWaveADPCM::Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) +{ + if ( NumChannels() == 1 ) + pDevice->Mix16Mono( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + else + pDevice->Mix16Stereo( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); +} + + +static int error_sign_lut[] = { 0, 1, 2, 3, 4, 5, 6, 7, -8, -7, -6, -5, -4, -3, -2, -1 }; +static int error_coefficients_lut[] = { 230, 230, 230, 230, 307, 409, 512, 614, + 768, 614, 512, 409, 307, 230, 230, 230 }; + +//----------------------------------------------------------------------------- +// Purpose: ADPCM decompress a single block of 1-channel audio +// Input : *pOut - output buffer 16-bit +// *pIn - input block +// count - number of samples to decode (to support partial blocks) +//----------------------------------------------------------------------------- +void CAudioMixerWaveADPCM::DecompressBlockMono( short *pOut, const char *pIn, int count ) +{ + int pred = *pIn++; + int co1 = m_pCoefficients[pred].iCoef1; + int co2 = m_pCoefficients[pred].iCoef2; + + // read initial delta + int delta = *((short *)pIn); + pIn += 2; + + // read initial samples for prediction + int samp1 = *((short *)pIn); + pIn += 2; + + int samp2 = *((short *)pIn); + pIn += 2; + + // write out the initial samples (stored in reverse order) + *pOut++ = (short)samp2; + *pOut++ = (short)samp1; + + // subtract the 2 samples in the header + count -= 2; + + // this is a toggle to read nibbles, first nibble is high + int high = 1; + + int error, sample=0; + + // now process the block + while ( count ) + { + // read the error nibble from the input stream + if ( high ) + { + sample = (unsigned char) (*pIn++); + // high nibble + error = sample >> 4; + // cache low nibble for next read + sample = sample & 0xf; + // Next read is from cache, not stream + high = 0; + } + else + { + // stored in previous read (low nibble) + error = sample; + // next read is from stream + high = 1; + } + // convert to signed with LUT + int errorSign = error_sign_lut[error]; + + // interpolate the new sample + int predSample = (samp1 * co1) + (samp2 * co2); + // coefficients are fixed point 8-bit, so shift back to 16-bit integer + predSample >>= 8; + + // Add in current error estimate + predSample += (errorSign * delta); + + // Correct error estimate + delta = (delta * error_coefficients_lut[error]) >> 8; + // Clamp error estimate + if ( delta < 16 ) + delta = 16; + + // clamp + if ( predSample > 32767L ) + predSample = 32767L; + else if ( predSample < -32768L ) + predSample = -32768L; + + // output + *pOut++ = (short)predSample; + // move samples over + samp2 = samp1; + samp1 = predSample; + + count--; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Decode a single block of stereo ADPCM audio +// Input : *pOut - 16-bit output buffer +// *pIn - ADPCM encoded block data +// count - number of sample pairs to decode +//----------------------------------------------------------------------------- +void CAudioMixerWaveADPCM::DecompressBlockStereo( short *pOut, const char *pIn, int count ) +{ + int pred[2], co1[2], co2[2]; + int i; + + for ( i = 0; i < 2; i++ ) + { + pred[i] = *pIn++; + co1[i] = m_pCoefficients[pred[i]].iCoef1; + co2[i] = m_pCoefficients[pred[i]].iCoef2; + } + + int delta[2], samp1[2], samp2[2]; + + for ( i = 0; i < 2; i++, pIn += 2 ) + { + // read initial delta + delta[i] = *((short *)pIn); + } + + // read initial samples for prediction + for ( i = 0; i < 2; i++, pIn += 2 ) + { + samp1[i] = *((short *)pIn); + } + for ( i = 0; i < 2; i++, pIn += 2 ) + { + samp2[i] = *((short *)pIn); + } + + // write out the initial samples (stored in reverse order) + *pOut++ = (short)samp2[0]; // left + *pOut++ = (short)samp2[1]; // right + *pOut++ = (short)samp1[0]; // left + *pOut++ = (short)samp1[1]; // right + + // subtract the 2 samples in the header + count -= 2; + + // this is a toggle to read nibbles, first nibble is high + int high = 1; + + int error, sample=0; + + // now process the block + while ( count ) + { + for ( i = 0; i < 2; i++ ) + { + // read the error nibble from the input stream + if ( high ) + { + sample = (unsigned char) (*pIn++); + // high nibble + error = sample >> 4; + // cache low nibble for next read + sample = sample & 0xf; + // Next read is from cache, not stream + high = 0; + } + else + { + // stored in previous read (low nibble) + error = sample; + // next read is from stream + high = 1; + } + // convert to signed with LUT + int errorSign = error_sign_lut[error]; + + // interpolate the new sample + int predSample = (samp1[i] * co1[i]) + (samp2[i] * co2[i]); + // coefficients are fixed point 8-bit, so shift back to 16-bit integer + predSample >>= 8; + + // Add in current error estimate + predSample += (errorSign * delta[i]); + + // Correct error estimate + delta[i] = (delta[i] * error_coefficients_lut[error]) >> 8; + // Clamp error estimate + if ( delta[i] < 16 ) + delta[i] = 16; + + // clamp + if ( predSample > 32767L ) + predSample = 32767L; + else if ( predSample < -32768L ) + predSample = -32768L; + + // output + *pOut++ = (short)predSample; + // move samples over + samp2[i] = samp1[i]; + samp1[i] = predSample; + } + count--; + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Read data from the source and pass it to the appropriate decompress +// routine. +// Output : Returns true if data was decoded, false if none. +//----------------------------------------------------------------------------- +bool CAudioMixerWaveADPCM::DecodeBlock( void ) +{ + char tmpBlock[MAX_BLOCK_SIZE]; + char *pData; + int blockSize; + int firstSample; + + // fixup position with possible loop + CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source()); + m_offset = source.ConvertLoopedPosition( m_offset ); + + if ( m_offset >= m_totalBytes ) + { + // no more data + return false; + } + + // can only decode in block sized chunks + firstSample = m_offset % m_blockSize; + m_offset = m_offset - firstSample; + + // adpcm must calculate and request correct block size for proper decoding + // last block size may be truncated + blockSize = m_totalBytes - m_offset; + if ( blockSize > m_blockSize ) + { + blockSize = m_blockSize; + } + + // get requested data + int available = m_pData->ReadSourceData( (void **)(&pData), m_offset, blockSize, NULL ); + if ( available < blockSize ) + { + // pump to get all of requested data + int total = 0; + while ( available && total < blockSize ) + { + memcpy( tmpBlock + total, pData, available ); + total += available; + available = m_pData->ReadSourceData( (void **)(&pData), m_offset + total, blockSize - total, NULL ); + } + pData = tmpBlock; + available = total; + } + + if ( !available ) + { + // no more data + return false; + } + + // advance the file pointer + m_offset += available; + + int channelCount = NumChannels(); + + // this is sample pairs for stereo, samples for mono + m_sampleCount = m_pFormat->wSamplesPerBlock; + + // short block?, fixup sample count (2 samples per byte, divided by number of channels per sample set) + m_sampleCount -= ((m_blockSize - available) * 2) / channelCount; + + // new block, start at the first sample + m_samplePosition = firstSample; + + // no need to subclass for different channel counts... + if ( channelCount == 1 ) + { + DecompressBlockMono( m_pSamples, pData, m_sampleCount ); + } + else + { + DecompressBlockStereo( m_pSamples, pData, m_sampleCount ); + } + return true; +} + + +//----------------------------------------------------------------------------- +// Purpose: Read existing buffer or decompress a new block when necessary +// Input : **pData - output data pointer +// sampleCount - number of samples (or pairs) +// Output : int - available samples (zero to stop decoding) +//----------------------------------------------------------------------------- +int CAudioMixerWaveADPCM::GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + if ( m_samplePosition >= m_sampleCount ) + { + if ( !DecodeBlock() ) + return 0; + } + + if ( m_pSamples && m_samplePosition < m_sampleCount ) + { + *pData = (void *)(m_pSamples + m_samplePosition * NumChannels()); + int available = m_sampleCount - m_samplePosition; + if ( available > sampleCount ) + available = sampleCount; + + m_samplePosition += available; + + // update count of max samples loaded in CAudioMixerWave + CAudioMixerWave::m_sample_max_loaded += available; + + // update index of last sample loaded + CAudioMixerWave::m_sample_loaded_index += available; + + return available; + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Purpose: Seek to a new position in the file +// NOTE: In most cases, only call this once, and call it before playing +// any data. +// Input : newPosition - new position in the sample clocks of this sample +//----------------------------------------------------------------------------- +void CAudioMixerWaveADPCM::SetSampleStart( int newPosition ) +{ + // cascade to base wave to update sample counter + CAudioMixerWave::SetSampleStart( newPosition ); + + // which block is the desired starting sample in? + int blockStart = newPosition / m_pFormat->wSamplesPerBlock; + // how far into the block is the sample + int blockOffset = newPosition % m_pFormat->wSamplesPerBlock; + + // set the file position + m_offset = blockStart * m_blockSize; + + // NOTE: Must decode a block here to properly position the sample Index + // THIS MEANS YOU DON'T WANT TO CALL THIS ROUTINE OFTEN FOR ADPCM SOUNDS + DecodeBlock(); + + // limit to the samples decoded + if ( blockOffset < m_sampleCount ) + blockOffset = m_sampleCount; + + // set the new current position + m_samplePosition = blockOffset; +} + + +//----------------------------------------------------------------------------- +// Purpose: Abstract factory function for ADPCM mixers +// Input : *data - wave data access object +// channels - +// Output : CAudioMixer +//----------------------------------------------------------------------------- +CAudioMixer *CreateADPCMMixer( IWaveData *data ) +{ + return new CAudioMixerWaveADPCM( data ); +} diff --git a/engine/audio/private/snd_wave_mixer_adpcm.h b/engine/audio/private/snd_wave_mixer_adpcm.h new file mode 100644 index 0000000..298fe66 --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_adpcm.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_WAVE_MIXER_ADPCM_H +#define SND_WAVE_MIXER_ADPCM_H +#pragma once + + +class CAudioMixer; +class IWaveData; + +CAudioMixer *CreateADPCMMixer( IWaveData *data ); + +#endif // SND_WAVE_MIXER_ADPCM_H diff --git a/engine/audio/private/snd_wave_mixer_mp3.cpp b/engine/audio/private/snd_wave_mixer_mp3.cpp new file mode 100644 index 0000000..8eaa16f --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_mp3.cpp @@ -0,0 +1,238 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + + +#include "audio_pch.h" +#include "snd_mp3_source.h" +#include "snd_wave_mixer_mp3.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +#ifndef DEDICATED // have to test this because VPC is forcing us to compile this file. + +extern IVAudio *vaudio; + +CAudioMixerWaveMP3::CAudioMixerWaveMP3( IWaveData *data ) : CAudioMixerWave( data ) +{ + m_sampleCount = 0; + m_samplePosition = 0; + m_offset = 0; + m_delaySamples = 0; + m_headerOffset = 0; + m_pStream = NULL; + m_bStreamInit = false; + m_channelCount = 0; +} + + +CAudioMixerWaveMP3::~CAudioMixerWaveMP3( void ) +{ + if ( m_pStream ) + delete m_pStream; +} + + +void CAudioMixerWaveMP3::Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) +{ + Assert( IsReadyToMix() ); + if ( m_channelCount == 1 ) + pDevice->Mix16Mono( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + else + pDevice->Mix16Stereo( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); +} + + +// Some MP3 files are wrapped in ID3 +void CAudioMixerWaveMP3::GetID3HeaderOffset() +{ + char copyBuf[AUDIOSOURCE_COPYBUF_SIZE]; + byte *pData; + + int bytesRead = m_pData->ReadSourceData( (void **)&pData, 0, 10, copyBuf ); + if ( bytesRead < 10 ) + return; + + m_headerOffset = 0; + if (( pData[ 0 ] == 0x49 ) && + ( pData[ 1 ] == 0x44 ) && + ( pData[ 2 ] == 0x33 ) && + ( pData[ 3 ] < 0xff ) && + ( pData[ 4 ] < 0xff ) && + ( pData[ 6 ] < 0x80 ) && + ( pData[ 7 ] < 0x80 ) && + ( pData[ 8 ] < 0x80 ) && + ( pData[ 9 ] < 0x80 ) ) + { + // this is in id3 file + // compute the size of the wrapper and skip it + m_headerOffset = 10 + ( pData[9] | (pData[8]<<7) | (pData[7]<<14) | (pData[6]<<21) ); + } +} + +int CAudioMixerWaveMP3::StreamRequestData( void *pBuffer, int bytesRequested, int offset ) +{ + if ( offset < 0 ) + { + offset = m_offset; + } + else + { + m_offset = offset; + } + // read the data out of the source + int totalBytesRead = 0; + + if ( offset == 0 ) + { + // top of file, check for ID3 wrapper + GetID3HeaderOffset(); + } + + offset += m_headerOffset; // skip any id3 header/wrapper + + while ( bytesRequested > 0 ) + { + char *pOutputBuffer = (char *)pBuffer; + pOutputBuffer += totalBytesRead; + + void *pData = NULL; + int bytesRead = m_pData->ReadSourceData( &pData, offset + totalBytesRead, bytesRequested, pOutputBuffer ); + + if ( !bytesRead ) + break; + if ( bytesRead > bytesRequested ) + { + bytesRead = bytesRequested; + } + // if the source is buffering it, copy it to the MP3 decomp buffer + if ( pData != pOutputBuffer ) + { + memcpy( pOutputBuffer, pData, bytesRead ); + } + totalBytesRead += bytesRead; + bytesRequested -= bytesRead; + } + + m_offset += totalBytesRead; + return totalBytesRead; +} + +bool CAudioMixerWaveMP3::DecodeBlock() +{ + IAudioStream *pStream = GetStream(); + if ( !pStream ) + { + return false; + } + + m_sampleCount = pStream->Decode( m_samples, sizeof(m_samples) ); + m_samplePosition = 0; + return m_sampleCount > 0; +} + +IAudioStream *CAudioMixerWaveMP3::GetStream() +{ + if ( !m_bStreamInit ) + { + m_bStreamInit = true; + + if ( vaudio ) + { + m_pStream = vaudio->CreateMP3StreamDecoder( static_cast<IAudioStreamEvent *>(this) ); + } + else + { + Warning( "Attempting to play MP3 with no vaudio [ %s ]\n", m_pData->Source().GetFileName() ); + } + + if ( m_pStream ) + { + m_channelCount = m_pStream->GetOutputChannels(); + //Assert( m_pStream->GetOutputRate() == m_pData->Source().SampleRate() ); + } + + if ( !m_pStream ) + { + Warning( "Failed to create decoder for MP3 [ %s ]\n", m_pData->Source().GetFileName() ); + } + } + + return m_pStream; +} + +//----------------------------------------------------------------------------- +// Purpose: Read existing buffer or decompress a new block when necessary +// Input : **pData - output data pointer +// sampleCount - number of samples (or pairs) +// Output : int - available samples (zero to stop decoding) +//----------------------------------------------------------------------------- +int CAudioMixerWaveMP3::GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + if ( m_samplePosition >= m_sampleCount ) + { + if ( !DecodeBlock() ) + return 0; + } + + IAudioStream *pStream = GetStream(); + if ( !pStream ) + { + // Needed for channel count, and with a failed stream init we probably should fail to return data anyway. + return 0; + } + + if ( m_samplePosition < m_sampleCount ) + { + int sampleSize = pStream->GetOutputChannels() * 2; + *pData = (void *)(m_samples + m_samplePosition); + int available = m_sampleCount - m_samplePosition; + int bytesRequired = sampleCount * sampleSize; + if ( available > bytesRequired ) + available = bytesRequired; + + m_samplePosition += available; + + int samples_loaded = available / sampleSize; + + // update count of max samples loaded in CAudioMixerWave + + CAudioMixerWave::m_sample_max_loaded += samples_loaded; + + // update index of last sample loaded + + CAudioMixerWave::m_sample_loaded_index += samples_loaded; + + return samples_loaded; + } + + return 0; +} + + + +//----------------------------------------------------------------------------- +// Purpose: Seek to a new position in the file +// NOTE: In most cases, only call this once, and call it before playing +// any data. +// Input : newPosition - new position in the sample clocks of this sample +//----------------------------------------------------------------------------- +void CAudioMixerWaveMP3::SetSampleStart( int newPosition ) +{ + // UNDONE: Implement this? +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : delaySamples - +//----------------------------------------------------------------------------- +void CAudioMixerWaveMP3::SetStartupDelaySamples( int delaySamples ) +{ + m_delaySamples = delaySamples; +} + +#endif diff --git a/engine/audio/private/snd_wave_mixer_mp3.h b/engine/audio/private/snd_wave_mixer_mp3.h new file mode 100644 index 0000000..1138c25 --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_mp3.h @@ -0,0 +1,59 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Mixer for ADPCM encoded audio +// +//=============================================================================// + +#ifndef SND_WAVE_MIXER_MP3_H +#define SND_WAVE_MIXER_MP3_H +#pragma once + +#include "vaudio/ivaudio.h" + +static const int MP3_BUFFER_SIZE = 16384; + +class CAudioMixerWaveMP3 : public CAudioMixerWave, public IAudioStreamEvent +{ +public: + CAudioMixerWaveMP3( IWaveData *data ); + ~CAudioMixerWaveMP3( void ); + + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ); + virtual int GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + + // need to override this to fixup blocks + // UNDONE: This doesn't quite work with MP3 - we need a MP3 position, not a sample position + void SetSampleStart( int newPosition ); + + int GetPositionForSave() { return GetStream() ? GetStream()->GetPosition() : 0; } + void SetPositionFromSaved(int position) { if ( GetStream() ) GetStream()->SetPosition(position); } + + // IAudioStreamEvent + virtual int StreamRequestData( void *pBuffer, int bytesRequested, int offset ); + + virtual void SetStartupDelaySamples( int delaySamples ); + virtual int GetMixSampleSize() { return CalcSampleSize( 16, m_channelCount ); } + + virtual int GetStreamOutputRate() { return GetStream() ? GetStream()->GetOutputRate() : 0; } + +private: + IAudioStream *GetStream(); + bool DecodeBlock( void ); + void GetID3HeaderOffset(); + + // Lazily initialized, use GetStream + IAudioStream *m_pStream; + bool m_bStreamInit; + + char m_samples[MP3_BUFFER_SIZE]; + int m_sampleCount; + int m_samplePosition; + int m_channelCount; + int m_offset; + int m_delaySamples; + int m_headerOffset; +}; + +CAudioMixerWaveMP3 *CreateMP3Mixer( IWaveData *data ); + +#endif // SND_WAVE_MIXER_MP3_H diff --git a/engine/audio/private/snd_wave_mixer_private.h b/engine/audio/private/snd_wave_mixer_private.h new file mode 100644 index 0000000..3aae40c --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_private.h @@ -0,0 +1,74 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_WAVE_MIXER_PRIVATE_H +#define SND_WAVE_MIXER_PRIVATE_H +#pragma once + +#include "snd_audio_source.h" +#include "snd_wave_mixer.h" +#include "sound_private.h" +#include "snd_wave_source.h" + +class IWaveData; + +abstract_class CAudioMixerWave : public CAudioMixer +{ +public: + CAudioMixerWave( IWaveData *data ); + virtual ~CAudioMixerWave( void ); + + int MixDataToDevice( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ); + int SkipSamples( channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ); + bool ShouldContinueMixing( void ); + + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) = 0; + virtual int GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + + virtual CAudioSource* GetSource( void ); + virtual int GetSamplePosition( void ); + virtual float ModifyPitch( float pitch ); + virtual float GetVolumeScale( void ); + + // Move the current position to newPosition + virtual void SetSampleStart( int newPosition ); + + // End playback at newEndPosition + virtual void SetSampleEnd( int newEndPosition ); + + virtual void SetStartupDelaySamples( int delaySamples ); + + // private helper routines + + char * LoadMixBuffer( channel_t *pChannel, int sample_load_request, int *psamples_loaded, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + int MixDataToDevice_( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset, bool bSkipAllSamples ); + int GetSampleLoadRequest( double rate, int sampleCount, bool bInterpolated_pitch ); + + virtual bool IsReadyToMix(); + virtual int GetPositionForSave() { return GetSamplePosition(); } + virtual void SetPositionFromSaved( int savedPosition ) { SetSampleStart(savedPosition); } + +protected: + double m_fsample_index; // index of next sample to output + int m_sample_max_loaded; // count of total samples loaded - ie: the index of + // the next sample to be loaded. + int m_sample_loaded_index; // index of last sample loaded + + IWaveData *m_pData; + double m_forcedEndSample; + bool m_finished; + int m_delaySamples; +}; + + +#endif // SND_WAVE_MIXER_PRIVATE_H diff --git a/engine/audio/private/snd_wave_mixer_xma.cpp b/engine/audio/private/snd_wave_mixer_xma.cpp new file mode 100644 index 0000000..13f183c --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_xma.cpp @@ -0,0 +1,959 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: XMA Decoding +// +//=====================================================================================// + +#include "audio_pch.h" +#include "tier1/mempool.h" +#include "circularbuffer.h" +#include "tier1/utllinkedlist.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +//#define DEBUG_XMA + +// Failed attempt to allow mixer to request data that is immediately discarded +// to support < 0 delay samples +//#define ALLOW_SKIP_SAMPLES + +// XMA is supposed to decode at an ideal max of 512 mono samples every 4msec. +// XMA can only peel a max of 1984 stereo samples per poll request (if available). +// Max is not achievable and degrades based on quality settings, stereo, etc, but using these numbers for for calcs. +// 1984 stereo samples should be decoded by xma in 31 msec. +// 1984 stereo samples at 44.1Khz dictates a request every 45 msec. +// GetOutputData() must be clocked faster than 45 msec or samples will not be available. +// However, the XMA decoder must be serviced much faster. It was designed for 5 msec. +// 15 msec seems to be fast enough for XMA to decode enough to keep the smaller buffer sizes satisfied, and have slop for +/- 5 msec swings. + +// Need at least this amount of decoded pcm samples before mixing can commence. +// This needs to be able to cover the initial mix request, while a new decode cycle is in flight. +#define MIN_READYTOMIX ( ( 2 * XMA_POLL_RATE ) * 0.001f ) + +// number of samples that xma decodes +// must be 128 aligned for mono (1984 is hw max for stereo) +#define XMA_MONO_OUTPUT_BUFFER_SAMPLES 2048 +#define XMA_STEREO_OUTPUT_BUFFER_SAMPLES 1920 + +// for decoder input +// xma blocks are fetched from the datacache into one of these hw buffers for decoding +// must be in quantum units of XMA_BLOCK_SIZE +#define XMA_INPUT_BUFFER_SIZE ( 8 * XMA_BLOCK_SIZE ) + +// circular staging buffer to drain xma decoder and stage until mixer requests +// must be large enough to hold the slowest expected mixing frame worth of samples +#define PCM_STAGING_BUFFER_TIME 200 + +// xma physical heap, supplies xma input buffers for hw decoder +// each potential channel must be able to peel 2 buffers for driving xma decoder +#define XMA_PHYSICAL_HEAP_SIZE ( 2 * MAX_CHANNELS * XMA_INPUT_BUFFER_SIZE ) + +// in millseconds +#define MIX_IO_DATA_TIMEOUT 2000 // async i/o from dvd could be very late +#define MIX_DECODER_TIMEOUT 3000 // decoder might be very busy +#define MIX_DECODER_POLLING_LATENCY 5 // not faster than 5ms, or decoder will sputter + +// diagnostic errors +#define ERROR_IO_DATA_TIMEOUT -1 // async i/o taking too long to deliver xma blocks +#define ERROR_IO_TRUNCATED_BLOCK -2 // async i/o failed to deliver complete blocks +#define ERROR_IO_NO_XMA_DATA -3 // async i/o failed to deliver any block +#define ERROR_DECODER_TIMEOUT -4 // decoder taking too long to decode xma blocks +#define ERROR_OUT_OF_MEMORY -5 // not enough physical memory for xma blocks +#define ERROR_XMA_PARSE -6 // decoder barfed on xma blocks +#define ERROR_XMA_CANTLOCK -7 // hw not acting as expected +#define ERROR_XMA_CANTSUBMIT -8 // hw not acting as expected +#define ERROR_XMA_CANTRESUME -9 // hw not acting as expected +#define ERROR_XMA_NO_PCM_DATA -10 // no xma decoded pcm data ready +#define ERROR_NULL_BUFFER -11 // logic flaw, expected buffer is null + +const char *g_XMAErrorStrings[] = +{ + "Unknown Error Code", + "Async I/O Data Timeout", // ERROR_IO_DATA_TIMEOUT + "Async I/O Truncated Block", // ERROR_IO_TRUNCATED_BLOCK + "Async I/O Data Not Ready", // ERROR_IO_NO_XMA_DATA + "Decoder Timeout", // ERROR_DECODER_TIMEOUT + "Out Of Memory", // ERROR_OUT_OF_MEMORY + "XMA Parse", // ERROR_XMA_PARSE + "XMA Cannot Lock", // ERROR_XMA_CANTLOCK + "XMA Cannot Submit", // ERROR_XMA_CANTSUBMIT + "XMA Cannot Resume", // ERROR_XMA_CANTRESUME + "XMA No PCM Data Ready", // ERROR_XMA_NO_PCM_DATA + "NULL Buffer", // ERROR_NULL_BUFFER +}; + +class CXMAAllocator +{ +public: + static void *Alloc( int bytes ) + { + MEM_ALLOC_CREDIT(); + + return XMemAlloc( bytes, + MAKE_XALLOC_ATTRIBUTES( + 0, + false, + TRUE, + FALSE, + eXALLOCAllocatorId_XAUDIO, + XALLOC_PHYSICAL_ALIGNMENT_4K, + XALLOC_MEMPROTECT_WRITECOMBINE_LARGE_PAGES, + FALSE, + XALLOC_MEMTYPE_PHYSICAL ) ); + } + + static void Free( void *p ) + { + XMemFree( p, + MAKE_XALLOC_ATTRIBUTES( + 0, + false, + TRUE, + FALSE, + eXALLOCAllocatorId_XAUDIO, + XALLOC_PHYSICAL_ALIGNMENT_4K, + XALLOC_MEMPROTECT_WRITECOMBINE_LARGE_PAGES, + FALSE, + XALLOC_MEMTYPE_PHYSICAL ) ); + } +}; + +// for XMA decoding, fixed size allocations aligned to 4K from a single physical heap +CAlignedMemPool< XMA_INPUT_BUFFER_SIZE, 4096, XMA_PHYSICAL_HEAP_SIZE, CXMAAllocator > g_XMAMemoryPool; + +ConVar snd_xma_spew_warnings( "snd_xma_spew_warnings", "0" ); +ConVar snd_xma_spew_startup( "snd_xma_spew_startup", "0" ); +ConVar snd_xma_spew_mixers( "snd_xma_spew_mixers", "0" ); +ConVar snd_xma_spew_decode( "snd_xma_spew_decode", "0" ); +ConVar snd_xma_spew_drain( "snd_xma_spew_drain", "0" ); +#ifdef DEBUG_XMA +ConVar snd_xma_record( "snd_xma_record", "0" ); +ConVar snd_xma_spew_errors( "snd_xma_spew_errors", "0" ); +#endif + +//----------------------------------------------------------------------------- +// Purpose: Mixer for ADPCM encoded audio +//----------------------------------------------------------------------------- +class CAudioMixerWaveXMA : public CAudioMixerWave +{ +public: + typedef CAudioMixerWave BaseClass; + + CAudioMixerWaveXMA( IWaveData *data, int initialStreamPosition ); + ~CAudioMixerWaveXMA( void ); + + virtual void Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ); + + virtual int GetOutputData( void **pData, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + + virtual void SetSampleStart( int newPosition ); + virtual int GetPositionForSave(); + virtual void SetPositionFromSaved( int savedPosition ); + + virtual int GetMixSampleSize() { return CalcSampleSize( 16, m_NumChannels ); } + + virtual bool IsReadyToMix(); + virtual bool ShouldContinueMixing(); + +private: + int GetXMABlocksAndSubmitToDecoder( bool bDecoderLocked ); + int UpdatePositionForLooping( int *pNumRequestedSamples ); + int ServiceXMADecoder( bool bForceUpdate ); + int GetPCMSamples( int numRequested, char *pData ); + + XMAPLAYBACK *m_pXMAPlayback; + + // input buffers, encoded xma + byte *m_pXMABuffers[2]; + int m_XMABufferIndex; + + // output buffer, decoded pcm samples, a staging circular buffer, waiting for mixer requests + // due to staging nature, contains decoded samples from multiple input buffers + CCircularBuffer *m_pPCMSamples; + + int m_SampleRate; + int m_NumChannels; + // maximum possible decoded samples + int m_SampleCount; + + // decoded sample position + int m_SamplePosition; + // current data marker + int m_LastDataOffset; + int m_DataOffset; + // total bytes of data + int m_TotalBytes; + +#if defined( ALLOW_SKIP_SAMPLES ) + // number of samples to throwaway + int m_SkipSamples; +#endif + + // timers + unsigned int m_StartTime; + unsigned int m_LastDrainTime; + unsigned int m_LastPollTime; + + int m_hMixerList; + int m_Error; + + unsigned int m_bStartedMixing : 1; + unsigned int m_bFinished : 1; + unsigned int m_bLooped : 1; +}; + +CUtlFixedLinkedList< CAudioMixerWaveXMA * > g_XMAMixerList; + +CON_COMMAND( snd_xma_info, "Spew XMA Info" ) +{ + Msg( "XMA Memory:\n" ); + Msg( " Blocks Allocated: %d\n", g_XMAMemoryPool.NumAllocated() ); + Msg( " Blocks Free: %d\n", g_XMAMemoryPool.NumFree() ); + Msg( " Total Bytes: %d\n", g_XMAMemoryPool.BytesTotal() ); + Msg( "Active XMA Mixers: %d\n", g_XMAMixerList.Count() ); + for ( int hMixer = g_XMAMixerList.Head(); hMixer != g_XMAMixerList.InvalidIndex(); hMixer = g_XMAMixerList.Next( hMixer ) ) + { + CAudioMixerWaveXMA *pXMAMixer = g_XMAMixerList[hMixer]; + Msg( " rate:%5d ch:%1d '%s'\n", pXMAMixer->GetSource()->SampleRate(), pXMAMixer->GetSource()->IsStereoWav() ? 2 : 1, pXMAMixer->GetSource()->GetFileName() ); + } +} + +CAudioMixerWaveXMA::CAudioMixerWaveXMA( IWaveData *data, int initialStreamPosition ) : CAudioMixerWave( data ) +{ + Assert( dynamic_cast<CAudioSourceWave *>(&m_pData->Source()) != NULL ); + + m_Error = 0; + + m_NumChannels = m_pData->Source().IsStereoWav() ? 2 : 1; + m_SampleRate = m_pData->Source().SampleRate(); + m_bLooped = m_pData->Source().IsLooped(); + m_SampleCount = m_pData->Source().SampleCount(); + m_TotalBytes = m_pData->Source().DataSize(); + +#if defined( ALLOW_SKIP_SAMPLES ) + m_SkipSamples = 0; +#endif + + m_LastDataOffset = initialStreamPosition; + m_DataOffset = initialStreamPosition; + m_SamplePosition = 0; + if ( initialStreamPosition ) + { + m_SamplePosition = m_pData->Source().StreamToSamplePosition( initialStreamPosition ); + + CAudioMixerWave::m_sample_loaded_index = m_SamplePosition; + CAudioMixerWave::m_sample_max_loaded = m_SamplePosition + 1; + } + + m_bStartedMixing = false; + m_bFinished = false; + + m_StartTime = 0; + m_LastPollTime = 0; + m_LastDrainTime = 0; + + m_pXMAPlayback = NULL; + m_pPCMSamples = NULL; + + m_pXMABuffers[0] = NULL; + m_pXMABuffers[1] = NULL; + m_XMABufferIndex = 0; + + m_hMixerList = g_XMAMixerList.AddToTail( this ); + +#ifdef DEBUG_XMA + if ( snd_xma_record.GetBool() ) + { + WaveCreateTmpFile( "debug.wav", m_SampleRate, 16, m_NumChannels ); + } +#endif + + if ( snd_xma_spew_mixers.GetBool() ) + { + Msg( "XMA: 0x%8.8x (%2d), Mixer Alloc, '%s'\n", (unsigned int)this, g_XMAMixerList.Count(), m_pData->Source().GetFileName() ); + } +} + +CAudioMixerWaveXMA::~CAudioMixerWaveXMA( void ) +{ + if ( m_pXMAPlayback ) + { + XMAPlaybackDestroy( m_pXMAPlayback ); + + g_XMAMemoryPool.Free( m_pXMABuffers[0] ); + if ( m_pXMABuffers[1] ) + { + g_XMAMemoryPool.Free( m_pXMABuffers[1] ); + } + } + + if ( m_pPCMSamples ) + { + FreeCircularBuffer( m_pPCMSamples ); + } + + g_XMAMixerList.Remove( m_hMixerList ); + + if ( snd_xma_spew_mixers.GetBool() ) + { + Msg( "XMA: 0x%8.8x (%2d), Mixer Freed, '%s'\n", (unsigned int)this, g_XMAMixerList.Count(), m_pData->Source().GetFileName() ); + } +} + +void CAudioMixerWaveXMA::Mix( IAudioDevice *pDevice, channel_t *pChannel, void *pData, int outputOffset, int inputOffset, fixedint fracRate, int outCount, int timecompress ) +{ + if ( m_NumChannels == 1 ) + { + pDevice->Mix16Mono( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } + else + { + pDevice->Mix16Stereo( pChannel, (short *)pData, outputOffset, inputOffset, fracRate, outCount, timecompress ); + } +} + +//----------------------------------------------------------------------------- +// Looping is achieved in two passes to provide a circular view of the linear data. +// Pass1: Clamps a sample request to the end of data. +// Pass2: Snaps to the loop start, and returns the number of samples to discard, could be 0, +// up to the expected loop sample position. +// Returns the number of samples to discard, or 0. +//----------------------------------------------------------------------------- +int CAudioMixerWaveXMA::UpdatePositionForLooping( int *pNumRequestedSamples ) +{ + if ( !m_bLooped ) + { + // not looping, no fixups + return 0; + } + + int numLeadingSamples; + int numTrailingSamples; + CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source()); + int loopSampleStart = source.GetLoopingInfo( NULL, &numLeadingSamples, &numTrailingSamples ); + + int numRemainingSamples = ( m_SampleCount - numTrailingSamples ) - m_SamplePosition; + + // possibly straddling the end of data (and thus about to loop) + // want to split the straddle into two regions, due to loops possibly requiring a trailer and leader of discarded samples + if ( numRemainingSamples > 0 ) + { + // first region, all the remaining samples, clamped until end of desired data + *pNumRequestedSamples = min( *pNumRequestedSamples, numRemainingSamples ); + + // nothing to discard + return 0; + } + else if ( numRemainingSamples == 0 ) + { + // at exact end of desired data, snap the sample position back + // the position will be correct AFTER discarding decoded trailing and leading samples + m_SamplePosition = loopSampleStart; + + // clamp the request + numRemainingSamples = ( m_SampleCount - numTrailingSamples ) - m_SamplePosition; + *pNumRequestedSamples = min( *pNumRequestedSamples, numRemainingSamples ); + + // flush these samples so the sample position is the real loop sample starting position + return numTrailingSamples + numLeadingSamples; + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Get and submit XMA block(s). The decoder must stay blocks ahead of mixer +// so the decoded samples are available for peeling. +// An XMA file is thus treated as a series of fixed size large buffers (multiple xma blocks), +// which are streamed in sequentially. The XMA buffers may be delayed from the +// audio data cache due to async i/o latency. +// Returns < 0 if error, 0 if no decode started, 1 if decode submitted. +//----------------------------------------------------------------------------- +int CAudioMixerWaveXMA::GetXMABlocksAndSubmitToDecoder( bool bDecoderIsLocked ) +{ + int status = 0; + + if ( m_DataOffset >= m_TotalBytes ) + { + if ( !m_bLooped ) + { + // end of file, no more data to decode + // not an error, because decoder finishes long before samples drained + return 0; + } + + // start from beginning of loop + CAudioSourceWave &source = reinterpret_cast<CAudioSourceWave &>(m_pData->Source()); + source.GetLoopingInfo( &m_DataOffset, NULL, NULL ); + m_DataOffset *= XMA_BLOCK_SIZE; + } + + HRESULT hr; + bool bLocked = false; + if ( !bDecoderIsLocked ) + { + // decoder must be locked before any access + hr = XMAPlaybackRequestModifyLock( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + status = ERROR_XMA_CANTLOCK; + goto cleanUp; + } + + hr = XMAPlaybackWaitUntilModifyLockObtained( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + status = ERROR_XMA_CANTLOCK; + goto cleanUp; + } + bLocked = true; + } + + // the input buffer can never be less than a single xma block (buffer size is multiple blocks) + int bufferSize = min( m_TotalBytes - m_DataOffset, XMA_INPUT_BUFFER_SIZE ); + if ( !bufferSize ) + { + // EOF + goto cleanUp; + } + Assert( !( bufferSize % XMA_BLOCK_SIZE ) ); + + byte *pXMABuffer = m_pXMABuffers[m_XMABufferIndex & 0x01]; + if ( !pXMABuffer ) + { + // shouldn't happen, buffer should have been allocated + Assert( 0 ); + status = ERROR_NULL_BUFFER; + goto cleanUp; + } + + if ( !XMAPlaybackQueryReadyForMoreData( m_pXMAPlayback, 0 ) || XMAPlaybackQueryInputDataPending( m_pXMAPlayback, 0, pXMABuffer ) ) + { + // decoder too saturated for more data or + // decoder still decoding from input hw buffer + goto cleanUp; + } + + // get xma block(s) + // pump to get all of requested data + char *pData; + int total = 0; + while ( total < bufferSize ) + { + int available = m_pData->ReadSourceData( (void **)&pData, m_DataOffset, bufferSize - total, NULL ); + if ( !available ) + break; + + // aggregate into hw buffer + V_memcpy( pXMABuffer + total, pData, available ); + + m_DataOffset += available; + total += available; + } + if ( total != bufferSize ) + { + if ( !total ) + { + // failed to get any data, could be async latency or file error + status = ERROR_IO_NO_XMA_DATA; + } + else + { + // failed to get complete xma block(s) + status = ERROR_IO_TRUNCATED_BLOCK; + } + goto cleanUp; + } + + // track the currently submitted offset + // this is used as a cheap method for save/restore because an XMA seek table is not available + m_LastDataOffset = m_DataOffset - total; + + // start decoding the block(s) in the hw buffer + hr = XMAPlaybackSubmitData( m_pXMAPlayback, 0, pXMABuffer, bufferSize ); + if ( FAILED( hr ) ) + { + // failed to start decoder + status = ERROR_XMA_CANTSUBMIT; + goto cleanUp; + } + + // decode submitted + status = 1; + + // advance to next buffer + m_XMABufferIndex++; + + if ( snd_xma_spew_decode.GetBool() ) + { + Msg( "XMA: 0x%8.8x, XMABuffer: 0x%8.8x, BufferSize: %d, NextDataOffset: %d, %s\n", (unsigned int)this, pXMABuffer, bufferSize, m_DataOffset, m_pData->Source().GetFileName() ); + } + +cleanUp: + if ( bLocked ) + { + // release the lock and let the decoder run + hr = XMAPlaybackResumePlayback( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + status = ERROR_XMA_CANTRESUME; + } + } + + return status; +} + +//----------------------------------------------------------------------------- +// Drain the XMA Decoder into the staging circular buffer of PCM for mixer. +// Fetch new XMA samples for the decoder. +//----------------------------------------------------------------------------- +int CAudioMixerWaveXMA::ServiceXMADecoder( bool bForceUpdate ) +{ + // allow decoder to work without being polled (lock causes a decoding stall) + // decoder must be allowed minimum operating latency + // the buffers are sized to compensate for the operating latency + if ( !bForceUpdate && ( Plat_MSTime() - m_LastPollTime <= MIX_DECODER_POLLING_LATENCY ) ) + { + return 0; + } + m_LastPollTime = Plat_MSTime(); + + // lock and pause the decoder to gain access + HRESULT hr = XMAPlaybackRequestModifyLock( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + m_Error = ERROR_XMA_CANTLOCK; + return -1; + } + + hr = XMAPlaybackWaitUntilModifyLockObtained( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + m_Error = ERROR_XMA_CANTLOCK; + return -1; + } + + DWORD dwParseError = XMAPlaybackGetParseError( m_pXMAPlayback, 0 ); + if ( dwParseError ) + { + if ( snd_xma_spew_warnings.GetBool() ) + { + Warning( "XMA: 0x%8.8x, Decoder Error, Parse: %d, '%s'\n", (unsigned int)this, dwParseError, m_pData->Source().GetFileName() ); + } + m_Error = ERROR_XMA_PARSE; + return -1; + } + +#ifdef DEBUG_XMA + if ( snd_xma_spew_errors.GetBool() ) + { + DWORD dwError = XMAPlaybackGetErrorBits( m_pXMAPlayback, 0 ); + if ( dwError ) + { + Warning( "XMA: 0x%8.8x, Playback Error: %d, '%s'\n", (unsigned int)this, dwError, m_pData->Source().GetFileName() ); + } + } +#endif + + int numNewSamples = XMAPlaybackQueryAvailableData( m_pXMAPlayback, 0 ); + int numMaxSamples = m_pPCMSamples->GetWriteAvailable()/( m_NumChannels*sizeof( short ) ); + int numSamples = min( numNewSamples, numMaxSamples ); + while ( numSamples ) + { + char *pPCMData = NULL; + int numSamplesDecoded = XMAPlaybackConsumeDecodedData( m_pXMAPlayback, 0, numSamples, (void**)&pPCMData ); + + // put into staging buffer, ready for mixer to drain + m_pPCMSamples->Write( pPCMData, numSamplesDecoded*m_NumChannels*sizeof( short ) ); + + numSamples -= numSamplesDecoded; + numNewSamples -= numSamplesDecoded; + } + + // queue up more blocks for the decoder + // the decoder will always finish ahead of the mixer, submit nothing, and the mixer will still be draining + int decodeStatus = GetXMABlocksAndSubmitToDecoder( true ); + if ( decodeStatus < 0 ) + { + m_Error = decodeStatus; + return -1; + } + + m_bFinished = ( numNewSamples == 0 ) && ( decodeStatus == 0 ) && XMAPlaybackIsIdle( m_pXMAPlayback, 0 ); + + // decoder was paused for access, let the decoder run + hr = XMAPlaybackResumePlayback( m_pXMAPlayback ); + if ( FAILED( hr ) ) + { + m_Error = ERROR_XMA_CANTRESUME; + return -1; + } + + return 1; +} + +//----------------------------------------------------------------------------- +// Drain the PCM staging buffer. +// Copy samples (numSamplesToCopy && pData). Return actual copied. +// Flush Samples (numSamplesToCopy && !pData). Return actual flushed. +// Query available number of samples (!numSamplesToCopy && !pData). Returns available. +//----------------------------------------------------------------------------- +int CAudioMixerWaveXMA::GetPCMSamples( int numSamplesToCopy, char *pData ) +{ + int numReadySamples = m_pPCMSamples->GetReadAvailable()/( m_NumChannels*sizeof( short ) ); + + // peel sequential samples from the stream's staging buffer + int numCopiedSamples = 0; + int numRequestedSamples = min( numSamplesToCopy, numReadySamples ); + if ( numRequestedSamples ) + { + if ( pData ) + { + // copy to caller + m_pPCMSamples->Read( pData, numRequestedSamples*m_NumChannels*sizeof( short ) ); + pData += numRequestedSamples*m_NumChannels*sizeof( short ); + } + else + { + // flush + m_pPCMSamples->Advance( numRequestedSamples*m_NumChannels*sizeof( short ) ); + } + + numCopiedSamples += numRequestedSamples; + } + + if ( snd_xma_spew_drain.GetBool() ) + { + char *pOperation = ( numSamplesToCopy && !pData ) ? "Flushed" : "Copied"; + Msg( "XMA: 0x%8.8x, SamplePosition: %d, Ready: %d, Requested: %d, %s: %d, Elapsed: %d ms '%s'\n", + (unsigned int)this, m_SamplePosition, numReadySamples, numSamplesToCopy, pOperation, numCopiedSamples, Plat_MSTime() - m_LastDrainTime, m_pData->Source().GetFileName() ); + } + m_LastDrainTime = Plat_MSTime(); + + if ( numSamplesToCopy ) + { + // could be actual flushed or actual copied + return numCopiedSamples; + } + + if ( !pData ) + { + // satify query for available + return numReadySamples; + } + + return 0; +} + +//----------------------------------------------------------------------------- +// Stall mixing until initial buffer of decoded samples are available. +//----------------------------------------------------------------------------- +bool CAudioMixerWaveXMA::IsReadyToMix() +{ + // XMA mixing cannot be driven from the main thread + Assert( ThreadInMainThread() == false ); + + if ( m_Error ) + { + // error has been set + // let mixer try to get unavailable samples, which casues the real abort + return true; + } + + if ( m_bStartedMixing ) + { + // decoding process has started + return true; + } + + if ( !m_pXMAPlayback ) + { + // first time, finish setup + int numBuffers; + if ( m_bLooped || m_TotalBytes > XMA_INPUT_BUFFER_SIZE ) + { + // data will cascade through multiple buffers + numBuffers = 2; + } + else + { + // data can fit into a single buffer + numBuffers = 1; + } + + // xma data must be decoded from a hw friendly buffer + // pool should have buffers available + if ( g_XMAMemoryPool.BytesAllocated() != numBuffers * g_XMAMemoryPool.ChunkSize() ) + { + for ( int i = 0; i < numBuffers; i++ ) + { + m_pXMABuffers[i] = (byte*)g_XMAMemoryPool.Alloc(); + } + + XMA_PLAYBACK_INIT xmaPlaybackInit = { 0 }; + xmaPlaybackInit.sampleRate = m_SampleRate; + xmaPlaybackInit.channelCount = m_NumChannels; + xmaPlaybackInit.subframesToDecode = 4; + xmaPlaybackInit.outputBufferSizeInSamples = ( m_NumChannels == 2 ) ? XMA_STEREO_OUTPUT_BUFFER_SAMPLES : XMA_MONO_OUTPUT_BUFFER_SAMPLES; + XMAPlaybackCreate( 1, &xmaPlaybackInit, 0, &m_pXMAPlayback ); + + int stagingSize = PCM_STAGING_BUFFER_TIME * m_SampleRate * m_NumChannels * sizeof( short ) * 0.001f; + m_pPCMSamples = AllocateCircularBuffer( AlignValue( stagingSize, 4 ) ); + } + else + { + // too many sounds playing, no xma buffers free + m_Error = ERROR_OUT_OF_MEMORY; + return true; + } + + m_StartTime = Plat_MSTime(); + } + + // waiting for samples + // allow decoder to work without being polled (lock causes a decoding stall) + if ( Plat_MSTime() - m_LastPollTime <= MIX_DECODER_POLLING_LATENCY ) + { + return false; + } + m_LastPollTime = Plat_MSTime(); + + // must have buffers in flight before mixing can begin + if ( m_DataOffset == m_LastDataOffset ) + { + // keep trying to get data, async i/o has some allowable latency + int decodeStatus = GetXMABlocksAndSubmitToDecoder( false ); + if ( decodeStatus < 0 && decodeStatus != ERROR_IO_NO_XMA_DATA ) + { + m_Error = decodeStatus; + return true; + } + else if ( !decodeStatus || decodeStatus == ERROR_IO_NO_XMA_DATA ) + { + // async streaming latency could be to blame, check watchdog + if ( Plat_MSTime() - m_StartTime >= MIX_IO_DATA_TIMEOUT ) + { + m_Error = ERROR_IO_DATA_TIMEOUT; + } + return false; + } + } + + // get the available samples ready for immediate mixing + if ( ServiceXMADecoder( true ) < 0 ) + { + return true; + } + + // can't mix until we have a minimum threshold of data or the decoder is finished + int minSamplesNeeded = m_bFinished ? 0 : MIN_READYTOMIX * m_SampleRate; +#if defined( ALLOW_SKIP_SAMPLES ) + minSamplesNeeded += m_bFinished ? 0 : m_SkipSamples; +#endif + + int numReadySamples = GetPCMSamples( 0, NULL ); + if ( numReadySamples > minSamplesNeeded ) + { + // decoder has samples ready for draining + m_bStartedMixing = true; + if ( snd_xma_spew_startup.GetBool() ) + { + Msg( "XMA: 0x%8.8x, Startup Latency: %d ms, Samples Ready: %d, '%s'\n", (unsigned int)this, Plat_MSTime() - m_StartTime, numReadySamples, m_pData->Source().GetFileName() ); + } + return true; + } + + if ( Plat_MSTime() - m_StartTime >= MIX_DECODER_TIMEOUT ) + { + m_Error = ERROR_DECODER_TIMEOUT; + } + + // on startup error, let mixer start and get unavailable samples, and abort + // otherwise hold off mixing until samples arrive + return ( m_Error != 0 ); +} + +//----------------------------------------------------------------------------- +// Returns true to mix, false to stop mixer completely. Called after +// mixer requests samples. +//----------------------------------------------------------------------------- +bool CAudioMixerWaveXMA::ShouldContinueMixing() +{ + if ( !IsRetail() && m_Error && snd_xma_spew_warnings.GetBool() ) + { + const char *pErrorString; + if ( m_Error < 0 && -m_Error < ARRAYSIZE( g_XMAErrorStrings ) ) + { + pErrorString = g_XMAErrorStrings[-m_Error]; + } + else + { + pErrorString = g_XMAErrorStrings[0]; + } + Warning( "XMA: 0x%8.8x, Mixer Aborted: %s, SamplePosition: %d/%d, DataOffset: %d/%d, '%s'\n", + (unsigned int)this, pErrorString, m_SamplePosition, m_SampleCount, m_DataOffset, m_TotalBytes, m_pData->Source().GetFileName() ); + } + + // an error condition is fatal to mixer + return ( m_Error == 0 && BaseClass::ShouldContinueMixing() ); +} + +//----------------------------------------------------------------------------- +// Read existing buffer or decompress a new block when necessary. +// If no samples can be fetched, returns 0, which hints the mixer to a pending shutdown state. +// This routines operates in large buffer quantums, and nothing smaller. +// XMA decode performance severly degrades if the lock is too frequent. +//----------------------------------------------------------------------------- +int CAudioMixerWaveXMA::GetOutputData( void **pData, int numSamplesToCopy, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + if ( m_Error ) + { + // mixer will eventually shutdown + return 0; + } + + if ( !m_bStartedMixing ) + { +#if defined( ALLOW_SKIP_SAMPLES ) + int numMaxSamples = AUDIOSOURCE_COPYBUF_SIZE/( m_NumChannels * sizeof( short ) ); + numSamplesToCopy = min( numSamplesToCopy, numMaxSamples ); + m_SkipSamples += numSamplesToCopy; + + // caller requesting data before mixing has commenced + V_memset( copyBuf, 0, numSamplesToCopy ); + *pData = (void*)copyBuf; + return numSamplesToCopy; +#else + // not allowed, GetOutputData() should only be called by the mixing loop + Assert( 0 ); + return 0; +#endif + } + + // XMA mixing cannot be driven from the main thread + Assert( ThreadInMainThread() == false ); + + // needs to be clocked at regular intervals + if ( ServiceXMADecoder( false ) < 0 ) + { + return 0; + } + +#if defined( ALLOW_SKIP_SAMPLES ) + if ( m_SkipSamples > 0 ) + { + // flush whatever is available + // ignore + m_SkipSamples -= GetPCMSamples( m_SkipSamples, NULL ); + if ( m_SkipSamples != 0 ) + { + // not enough decoded data ready to flush + // must flush these samples to maintain proper position + m_Error = ERROR_XMA_NO_PCM_DATA; + return 0; + } + } +#endif + + // loopback may require flushing some decoded samples + int numRequestedSamples = numSamplesToCopy; + int numDiscardSamples = UpdatePositionForLooping( &numRequestedSamples ); + if ( numDiscardSamples > 0 ) + { + // loopback requires discarding samples to converge to expected looppoint + numDiscardSamples -= GetPCMSamples( numDiscardSamples, NULL ); + if ( numDiscardSamples != 0 ) + { + // not enough decoded data ready to flush + // must flush these samples to achieve looping + m_Error = ERROR_XMA_NO_PCM_DATA; + return 0; + } + } + + // can only drain as much as can be copied to caller + int numMaxSamples = AUDIOSOURCE_COPYBUF_SIZE/( m_NumChannels * sizeof( short ) ); + numRequestedSamples = min( numRequestedSamples, numMaxSamples ); + + int numCopiedSamples = GetPCMSamples( numRequestedSamples, copyBuf ); + if ( numCopiedSamples ) + { + CAudioMixerWave::m_sample_max_loaded += numCopiedSamples; + CAudioMixerWave::m_sample_loaded_index += numCopiedSamples; + + // advance position by valid samples + m_SamplePosition += numCopiedSamples; + + *pData = (void*)copyBuf; + +#ifdef DEBUG_XMA + if ( snd_xma_record.GetBool() ) + { + WaveAppendTmpFile( "debug.wav", copyBuf, 16, numCopiedSamples * m_NumChannels ); + WaveFixupTmpFile( "debug.wav" ); + } +#endif + } + else + { + // no samples copied + if ( !m_bFinished && numRequestedSamples ) + { + // XMA latency error occurs when decoder not finished (not at EOF) and caller wanted samples but can't get any + if ( snd_xma_spew_warnings.GetInt() ) + { + Warning( "XMA: 0x%8.8x, No Decoded Data Ready: %d samples needed, '%s'\n", (unsigned int)this, numSamplesToCopy, m_pData->Source().GetFileName() ); + } + m_Error = ERROR_XMA_NO_PCM_DATA; + } + } + + return numCopiedSamples; +} + +//----------------------------------------------------------------------------- +// Purpose: Seek to a new position in the file +// NOTE: In most cases, only call this once, and call it before playing +// any data. +// Input : newPosition - new position in the sample clocks of this sample +//----------------------------------------------------------------------------- +void CAudioMixerWaveXMA::SetSampleStart( int newPosition ) +{ + // cannot support this + // this should be unused and thus not supporting + Assert( 0 ); +} + + +int CAudioMixerWaveXMA::GetPositionForSave() +{ + if ( m_bLooped ) + { + // A looped sample cannot be saved/restored because the decoded sample position, + // which is needed for loop calc, cannot ever be correctly restored without + // the XMA seek table. + return 0; + } + + // This is silly and totally wrong, but doing it anyways. + // The correct thing was to have the XMA seek table and use + // that to determine the correct packet. This is just a hopeful + // nearby approximation. Music did not have the seek table at + // the time of this code. The Seek table was added for vo + // restoration later. + return m_LastDataOffset; +} + +void CAudioMixerWaveXMA::SetPositionFromSaved( int savedPosition ) +{ + // Not used here. The Mixer creation will be given the initial startup offset. +} + +//----------------------------------------------------------------------------- +// Purpose: Abstract factory function for XMA mixers +//----------------------------------------------------------------------------- +CAudioMixer *CreateXMAMixer( IWaveData *data, int initialStreamPosition ) +{ + return new CAudioMixerWaveXMA( data, initialStreamPosition ); +} diff --git a/engine/audio/private/snd_wave_mixer_xma.h b/engine/audio/private/snd_wave_mixer_xma.h new file mode 100644 index 0000000..7ec7663 --- /dev/null +++ b/engine/audio/private/snd_wave_mixer_xma.h @@ -0,0 +1,24 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#ifndef SND_WAVE_MIXER_XMA_H +#define SND_WAVE_MIXER_XMA_H +#pragma once + +class CAudioMixer; +class IWaveData; + +// xma must be decoded as atomic blocks +#define XMA_BLOCK_SIZE ( 2 * 1024 ) + +// cannot be made slower than 15ms +// cannot be made faster than 5ms +// xma hardware needs be have stable clocking +#define XMA_POLL_RATE 15 + +CAudioMixer *CreateXMAMixer( IWaveData *data, int initialStreamPosition ); + +#endif // SND_WAVE_MIXER_XMA_H diff --git a/engine/audio/private/snd_wave_source.cpp b/engine/audio/private/snd_wave_source.cpp new file mode 100644 index 0000000..38c70a0 --- /dev/null +++ b/engine/audio/private/snd_wave_source.cpp @@ -0,0 +1,2816 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "audio_pch.h" +#include "snd_mp3_source.h" +#include "utlsymbol.h" +#include "checksum_crc.h" +#include "../../host.h" +#include "xwvfile.h" +#include "filesystem/IQueuedLoader.h" +#include "tier1/lzmaDecoder.h" +#include "tier2/fileutils.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// This determines how much data to pre-cache (will invalidate per-map caches if changed). +#define SND_ASYNC_LOOKAHEAD_SECONDS ( 0.125f ) + +extern ConVar snd_async_spew_blocking; +ConVar snd_async_minsize("snd_async_minsize", "262144"); + +// #define DEBUG_CHUNKS + +//----------------------------------------------------------------------------- +// Purpose: Report chunk error +// Input : id - chunk FOURCC +//----------------------------------------------------------------------------- +void ChunkError( unsigned int id ) +{ +#if defined( DEBUG_CHUNKS ) && defined( _DEBUG ) + if ( id == WAVE_LIST || id == WAVE_FACT ) + { + // unused chunks, not an error + return; + } + + char tmp[256]; + char idname[5]; + idname[4] = 0; + memcpy( idname, &id, 4 ); + + Q_snprintf( tmp, sizeof( tmp ), "Unhandled chunk %s\n", idname ); + Plat_DebugString( tmp ); +#endif +} + +//----------------------------------------------------------------------------- +// Purpose: Determine a true sample count for an ADPCM blob +//----------------------------------------------------------------------------- +int ADPCMSampleCount( ADPCMWAVEFORMAT *pFormat, int length ) +{ + // determine a true sample count + int nChannels = LittleWord( pFormat->wfx.nChannels ); + int wSamplesPerBlock = LittleWord( pFormat->wSamplesPerBlock ); + + int blockSize = (( wSamplesPerBlock - 2) * nChannels ) / 2; + blockSize += 7 * nChannels; + + int blockCount = length / blockSize; + int blockRem = length % blockSize; + + // total samples in complete blocks + int sampleCount = blockCount * wSamplesPerBlock; + + // add remaining in a short block + if ( blockRem ) + { + sampleCount += wSamplesPerBlock - (((blockSize - blockRem) * 2) / nChannels ); + } + + return sampleCount; +} + +//----------------------------------------------------------------------------- +// Purpose: Init to empty wave +//----------------------------------------------------------------------------- +CAudioSourceWave::CAudioSourceWave( CSfxTable *pSfx ) +{ + m_format = 0; + m_pHeader = NULL; + m_nHeaderSize = 0; + + // no looping + m_loopStart = -1; + + m_sampleSize = 1; + m_sampleCount = 0; + m_bits = 0; + m_channels = 0; + m_dataStart = 0; + m_dataSize = 0; + m_rate = 0; + + m_refCount = 0; + + m_pSfx = pSfx; +#ifdef _DEBUG + if ( m_pSfx ) + m_pDebugName = strdup( m_pSfx->getname() ); +#endif + + m_bNoSentence = false; + m_pTempSentence = NULL; + m_nCachedDataSize = 0; + m_bIsPlayOnce = false; + m_bIsSentenceWord = false; + + m_numDecodedSamples = 0; +} + +CAudioSourceWave::CAudioSourceWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) +{ + m_pSfx = pSfx; +#ifdef _DEBUG + if ( m_pSfx ) + m_pDebugName = strdup( m_pSfx->getname() ); +#endif + + m_refCount = 0; + + m_pHeader = NULL; + m_nHeaderSize = 0; + + if ( info->HeaderData() ) + { + m_pHeader = new char[ info->HeaderSize() ]; + Assert( m_pHeader ); + Q_memcpy( m_pHeader, info->HeaderData(), info->HeaderSize() ); + m_nHeaderSize = info->HeaderSize(); + } + + m_bits = info->Bits(); + m_channels = info->Channels(); + m_sampleSize = info->SampleSize(); + m_format = info->Format(); + m_dataStart = info->DataStart(); + m_dataSize = info->DataSize(); + m_rate = info->SampleRate(); + m_loopStart = info->LoopStart(); + m_sampleCount = info->SampleCount(); + m_numDecodedSamples = m_sampleCount; + + if ( m_format == WAVE_FORMAT_ADPCM && m_pHeader ) + { + m_numDecodedSamples = ADPCMSampleCount( (ADPCMWAVEFORMAT *)m_pHeader, m_sampleCount ); + } + + m_bNoSentence = false; + m_pTempSentence = NULL; + m_nCachedDataSize = 0; + m_bIsPlayOnce = false; + m_bIsSentenceWord = false; +} + + +CAudioSourceWave::~CAudioSourceWave( void ) +{ +#if _DEBUG + if ( !CanDelete() ) + Assert(0); +#endif + + // for non-standard waves, we store a copy of the header in RAM + delete[] m_pHeader; + delete m_pTempSentence; +} + +int CAudioSourceWave::GetType( void ) +{ + return AUDIO_SOURCE_WAV; +} + +void CAudioSourceWave::GetCacheData( CAudioSourceCachedInfo *info ) +{ + Assert( info->Type() == CAudioSource::AUDIO_SOURCE_WAV ); + + byte tempbuf[ 32768 ]; + int datalen = 0; + // NOTE GetStartupData has side-effects (...) hence the unconditional call + if ( GetStartupData( tempbuf, sizeof( tempbuf ), datalen ) && + info->s_bIsPrecacheSound && + datalen > 0 ) + { + byte *data = new byte[ datalen ]; + Q_memcpy( data, tempbuf, datalen ); + info->SetCachedDataSize( datalen ); + info->SetCachedData( data ); + } + + info->SetBits( m_bits ); + info->SetChannels( m_channels ); + info->SetSampleSize( m_sampleSize ); + info->SetFormat( m_format ); + info->SetDataStart( m_dataStart ); // offset of wave data chunk + info->SetDataSize( m_dataSize ); // size of wave data chunk + info->SetSampleRate( m_rate ); + info->SetLoopStart( m_loopStart ); + info->SetSampleCount( m_sampleCount ); + + if ( m_pTempSentence ) + { + CSentence *scopy = new CSentence; + *scopy = *m_pTempSentence; + info->SetSentence( scopy ); + + // Wipe it down to basically nothing + delete m_pTempSentence; + m_pTempSentence = NULL; + } + + if ( m_pHeader && m_nHeaderSize > 0 ) + { + byte *data = new byte[ m_nHeaderSize ]; + Q_memcpy( data, m_pHeader, m_nHeaderSize ); + info->SetHeaderSize( m_nHeaderSize ); + info->SetHeaderData( data ); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char const +//----------------------------------------------------------------------------- +char const *CAudioSourceWave::GetFileName() +{ + return m_pSfx ? m_pSfx->GetFileName() : "NULL m_pSfx"; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAudioSourceWave::IsAsyncLoad() +{ + VPROF("CAudioSourceWave::IsAsyncLoad"); + + if ( ( IsPC() || !IsX360() ) && !m_AudioCacheHandle.IsValid() ) + { + m_AudioCacheHandle.Get( GetType(), m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + } + + // If there's a bit of "cached data" then we don't have to lazy/async load (we still async load the remaining data, + // but we run from the cache initially) + if ( m_dataSize > snd_async_minsize.GetInt() ) + return true; + return ( m_nCachedDataSize > 0 ) ? false : true; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceWave::CheckAudioSourceCache() +{ + if ( IsX360() ) + { + // 360 does not use audio cache files + return; + } + + Assert( m_pSfx ); + + if ( !m_pSfx || !m_pSfx->IsPrecachedSound() ) + { + return; + } + + // This will "re-cache" this if it's not in this level's cache already + m_AudioCacheHandle.Get( GetType(), true, m_pSfx, &m_nCachedDataSize ); +} + +//----------------------------------------------------------------------------- +// Purpose: Init the wave data. +// Input : *pHeaderBuffer - the RIFF fmt chunk +// headerSize - size of that chunk +//----------------------------------------------------------------------------- +void CAudioSourceWave::Init( const char *pHeaderBuffer, int headerSize ) +{ + const WAVEFORMATEX *pHeader = (const WAVEFORMATEX *)pHeaderBuffer; + + // copy the relevant header data + m_format = LittleWord( pHeader->wFormatTag ); + m_bits = LittleWord( pHeader->wBitsPerSample ); + m_rate = LittleDWord( pHeader->nSamplesPerSec ); + m_channels = LittleWord( pHeader->nChannels ); + m_sampleSize = (m_bits * m_channels)/8; + + // this can never be zero -- other functions divide by this. + // this should never happen, but avoid crashing + if ( m_sampleSize <= 0 ) + { + m_sampleSize = 1; + } + + if ( m_format == WAVE_FORMAT_ADPCM ) + { + // For non-standard waves (like ADPCM) store the header, it has the decoding coefficients + m_pHeader = new char[headerSize]; + memcpy( m_pHeader, pHeader, headerSize ); + m_nHeaderSize = headerSize; + + // treat ADPCM sources as a file of bytes. They are decoded by the mixer + m_sampleSize = 1; + } +} + + +int CAudioSourceWave::SampleRate( void ) +{ + return m_rate; +} + + +//----------------------------------------------------------------------------- +// Purpose: Size of each sample +// Output : +//----------------------------------------------------------------------------- +int CAudioSourceWave::SampleSize( void ) +{ + return m_sampleSize; +} + +//----------------------------------------------------------------------------- +// Purpose: Total number of samples in this source +// Output : int +//----------------------------------------------------------------------------- +int CAudioSourceWave::SampleCount( void ) +{ + // caller wants real samples + return m_numDecodedSamples; +} + +int CAudioSourceWave::Format( void ) +{ + return m_format; +} + +int CAudioSourceWave::DataSize( void ) +{ + return m_dataSize; +} + +bool CAudioSourceWave::IsVoiceSource() +{ + if ( GetSentence() ) + { + if ( GetSentence()->GetVoiceDuck() ) + return true; + } + return false; +} + + +//----------------------------------------------------------------------------- +// Purpose: Do any sample conversion +// For 8 bit PCM, convert to signed because the mixing routine assumes this +// Input : *pData - pointer to sample data +// sampleCount - number of samples +//----------------------------------------------------------------------------- +void CAudioSourceWave::ConvertSamples( char *pData, int sampleCount ) +{ + if ( m_format == WAVE_FORMAT_PCM ) + { + if ( m_bits == 8 ) + { + for ( int i = 0; i < sampleCount*m_channels; i++ ) + { + *pData = (unsigned char)((int)((unsigned)*pData) - 128); + pData++; + } + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Parse base chunks +// Input : &walk - riff file to parse +// : chunkName - name of the chunk to parse +//----------------------------------------------------------------------------- +// UNDONE: Move parsing loop here and drop each chunk into a virtual function +// instead of this being virtual. +void CAudioSourceWave::ParseChunk( IterateRIFF &walk, int chunkName ) +{ + switch( chunkName ) + { + case WAVE_CUE: + ParseCueChunk( walk ); + break; + case WAVE_SAMPLER: + ParseSamplerChunk( walk ); + break; + case WAVE_VALVEDATA: + ParseSentence( walk ); + break; + default: + // unknown and don't care + ChunkError( walk.ChunkName() ); + break; + } +} + +bool CAudioSourceWave::IsLooped( void ) +{ + return (m_loopStart >= 0) ? true : false; +} + +bool CAudioSourceWave::IsStereoWav( void ) +{ + return (m_channels == 2) ? true : false; +} + +bool CAudioSourceWave::IsStreaming( void ) +{ + return false; +} + + +int CAudioSourceWave::GetCacheStatus( void ) +{ + return AUDIO_IS_LOADED; +} + +void CAudioSourceWave::CacheLoad( void ) +{ +} + +void CAudioSourceWave::CacheUnload( void ) +{ +} + +int CAudioSourceWave::ZeroCrossingBefore( int sample ) +{ + return sample; +} + + +int CAudioSourceWave::ZeroCrossingAfter( int sample ) +{ + return sample; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : &walk - +//----------------------------------------------------------------------------- +void CAudioSourceWave::ParseSentence( IterateRIFF &walk ) +{ + CUtlBuffer buf( 0, 0, CUtlBuffer::TEXT_BUFFER ); + + buf.EnsureCapacity( walk.ChunkSize() ); + walk.ChunkRead( buf.Base() ); + buf.SeekPut( CUtlBuffer::SEEK_HEAD, walk.ChunkSize() ); + + m_pTempSentence = new CSentence(); + Assert( m_pTempSentence ); + + m_pTempSentence->InitFromDataChunk( buf.Base(), buf.TellPut() ); + + // Throws all phonemes into one word, discards sentence memory, etc. + m_pTempSentence->MakeRuntimeOnly(); +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : CSentence +//----------------------------------------------------------------------------- +CSentence *CAudioSourceWave::GetSentence( void ) +{ + if ( IsX360() ) + { + return m_pTempSentence; + } + + // Already checked and this wav doesn't have sentence data... + if ( m_bNoSentence == true ) + { + return NULL; + } + + // Look up sentence from cache + CAudioSourceCachedInfo *info = m_AudioCacheHandle.FastGet(); + if ( !info ) + { + info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + } + Assert( info ); + if ( !info ) + { + m_bNoSentence = true; + return NULL; + } + + CSentence *sentence = info->Sentence(); + if ( !sentence ) + { + m_bNoSentence = true; + return NULL; + } + + if ( sentence->m_bIsValid ) + { + return sentence; + } + + m_bNoSentence = true; + + return NULL; +} + +const char *CAudioSourceWave::GetName() +{ + return m_pSfx ? m_pSfx->getname() : NULL; +} + +//----------------------------------------------------------------------------- +// Load a native xaudio or legacy wav +//----------------------------------------------------------------------------- +bool CAudioSourceWave::GetXboxAudioStartupData() +{ + CUtlBuffer buf; + char fileName[MAX_PATH]; + char tempFileName[MAX_PATH]; + + MEM_ALLOC_CREDIT(); + + // try native optimal xma wav file first + Q_StripExtension( m_pSfx->GetFileName(), tempFileName, sizeof( tempFileName ) ); + Q_snprintf( fileName, sizeof( fileName ), "sound\\%s.360.wav", tempFileName ); + if ( !g_pFullFileSystem->ReadFile( fileName, "GAME", buf, sizeof( xwvHeader_t ) ) ) + { + // not found, not supported + return false; + } + else + { + xwvHeader_t* pHeader = (xwvHeader_t *)buf.Base(); + if ( pHeader->id != XWV_ID || pHeader->version != XWV_VERSION ) + { + return false; + } + + if ( pHeader->format == XWV_FORMAT_XMA ) + { + m_format = WAVE_FORMAT_XMA; + } + else if ( pHeader->format == XWV_FORMAT_PCM ) + { + m_format = WAVE_FORMAT_PCM; + } + else + { + // unknown + return false; + } + + m_rate = pHeader->GetSampleRate(); + m_channels = pHeader->channels; + m_dataStart = pHeader->dataOffset; + m_dataSize = pHeader->dataSize; + + m_loopStart = pHeader->loopStart; + m_loopBlock = pHeader->loopBlock; + m_numLeadingSamples = pHeader->numLeadingSamples; + m_numTrailingSamples = pHeader->numTrailingSamples; + + if ( m_format == WAVE_FORMAT_XMA ) + { + // xma is compressed blocks, trick to fool system to treat data as bytes, not samples + // unfortunate, but callers must know xma context and provide offsets in samples or bytes + m_bits = 16; + m_sampleSize = 1; + m_sampleCount = m_dataSize; + } + else + { + m_bits = 16; + m_sampleSize = sizeof( short ) * m_channels; + m_sampleCount = m_dataSize / m_sampleSize; + } + + // keep true decoded samples because cannot be easily determined + m_numDecodedSamples = pHeader->numDecodedSamples; + + m_bNoSentence = true; + + CUtlBuffer fileBuffer; + if ( pHeader->staticDataSize ) + { + // get optional data + if ( !g_pFullFileSystem->ReadFile( fileName, "GAME", fileBuffer, pHeader->staticDataSize, sizeof( xwvHeader_t ) ) ) + { + return false; + } + + unsigned char *pData = (unsigned char *)fileBuffer.Base() + sizeof( xwvHeader_t ); + if ( pHeader->GetSeekTableSize() ) + { + // store off the seek table + m_nHeaderSize = pHeader->GetSeekTableSize(); + m_pHeader = new char[m_nHeaderSize]; + V_memcpy( m_pHeader, pData, m_nHeaderSize ); + + // advance past optional seek table + pData += m_nHeaderSize; + } + + if ( pHeader->vdatSize ) + { + m_pTempSentence = new CSentence(); + Assert( m_pTempSentence ); + m_bNoSentence = false; + + // vdat is precompiled into minimal binary format and possibly compressed + if ( CLZMA::IsCompressed( pData ) ) + { + // uncompress binary vdat and restore + CUtlBuffer targetBuffer; + int originalSize = CLZMA::GetActualSize( pData ); + targetBuffer.EnsureCapacity( originalSize ); + CLZMA::Uncompress( pData, (unsigned char *)targetBuffer.Base() ); + targetBuffer.SeekPut( CUtlBuffer::SEEK_HEAD, originalSize ); + m_pTempSentence->CacheRestoreFromBuffer( targetBuffer ); + } + else + { + m_pTempSentence->CacheRestoreFromBuffer( fileBuffer ); + } + } + } + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: Bastardized construction routine. This is just to avoid complex +// constructor functions so code can be shared more easily by sub-classes +// Input : *pFormatBuffer - RIFF header +// formatSize - header size +// &walk - RIFF file +//----------------------------------------------------------------------------- +void CAudioSourceWave::Setup( const char *pFormatBuffer, int formatSize, IterateRIFF &walk ) +{ + Init( pFormatBuffer, formatSize ); + + while ( walk.ChunkAvailable() ) + { + ParseChunk( walk, walk.ChunkName() ); + walk.ChunkNext(); + } +} + + +bool CAudioSourceWave::GetStartupData( void *dest, int destsize, int& bytesCopied ) +{ + bytesCopied = 0; + + char formatBuffer[1024]; + const char *pName = m_pSfx->GetFileName(); + InFileRIFF riff( pName, *g_pSndIO ); + + 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() ); + + int format = 0; + int formatSize = 0; + + // This chunk must be first as it contains the wave's format + // break out when we've parsed it + while ( walk.ChunkAvailable() && format == 0 ) + { + switch( walk.ChunkName() ) + { + case WAVE_FMT: + { + if ( walk.ChunkSize() <= sizeof( formatBuffer ) ) + { + walk.ChunkRead( formatBuffer ); + formatSize = walk.ChunkSize(); + format = ((WAVEFORMATEX *)formatBuffer)->wFormatTag; + if( ((WAVEFORMATEX *)formatBuffer)->wBitsPerSample > 16) + { + Warning("Unsupported %d-bit wave file %s\n", (int)((WAVEFORMATEX *)formatBuffer)->wBitsPerSample, pName); + } + } + } + break; + default: + { + ChunkError( walk.ChunkName() ); + } + break; + } + walk.ChunkNext(); + } + + // Not really a WAVE file or no format chunk, bail + if ( !format ) + { + return false; + } + + Setup( formatBuffer, formatSize, walk ); + + if ( !m_dataStart || !m_dataSize ) + { + // failed during setup + return false; + } + + // requesting precache snippet as leader for streaming startup latency + if ( destsize ) + { + int file = g_pSndIO->open( m_pSfx->GetFileName() ); + if ( !file ) + { + return false; + } + + int bytesNeeded = m_channels * ( m_bits >> 3 ) * m_rate * SND_ASYNC_LOOKAHEAD_SECONDS; + + // Round to multiple of 4 + bytesNeeded = ( bytesNeeded + 3 ) & ~3; + + bytesCopied = min( destsize, m_dataSize ); + bytesCopied = min( bytesNeeded, bytesCopied ); + + g_pSndIO->seek( file, m_dataStart ); + g_pSndIO->read( dest, bytesCopied, file ); + g_pSndIO->close( file ); + + // some samples need to be converted + ConvertSamples( (char *)dest, ( bytesCopied / m_sampleSize ) ); + } + + return true; +} + +//----------------------------------------------------------------------------- +// Purpose: parses loop information from a cue chunk +// Input : &walk - RIFF iterator +// Output : int loop start position +//----------------------------------------------------------------------------- +void CAudioSourceWave::ParseCueChunk( IterateRIFF &walk ) +{ + // Cue chunk as specified by RIFF format + // see $/research/jay/sound/riffnew.htm + struct + { + unsigned int dwName; + unsigned int dwPosition; + unsigned int fccChunk; + unsigned int dwChunkStart; + unsigned int dwBlockStart; + unsigned int dwSampleOffset; + } cue_chunk; + + int cueCount; + + // assume that the cue chunk stored in the wave is the start of the loop + // assume only one cue chunk, UNDONE: Test this assumption here? + cueCount = walk.ChunkReadInt(); + if ( cueCount > 0 ) + { + walk.ChunkReadPartial( &cue_chunk, sizeof(cue_chunk) ); + m_loopStart = LittleLong( cue_chunk.dwSampleOffset ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: parses loop information from a 'smpl' chunk +// Input : &walk - RIFF iterator +// Output : int loop start position +//----------------------------------------------------------------------------- +void CAudioSourceWave::ParseSamplerChunk( IterateRIFF &walk ) +{ + // Sampler chunk for MIDI instruments + // Parse loop info from this chunk too + struct SampleLoop + { + unsigned int dwIdentifier; + unsigned int dwType; + unsigned int dwStart; + unsigned int dwEnd; + unsigned int dwFraction; + unsigned int dwPlayCount; + }; + + struct + { + unsigned int dwManufacturer; + unsigned int dwProduct; + unsigned int dwSamplePeriod; + unsigned int dwMIDIUnityNote; + unsigned int dwMIDIPitchFraction; + unsigned int dwSMPTEFormat; + unsigned int dwSMPTEOffset; + unsigned int cSampleLoops; + unsigned int cbSamplerData; + struct SampleLoop Loops[1]; + } samplerChunk; + + // assume that the loop end is the sample end + // assume that only the first loop is relevant + + walk.ChunkReadPartial( &samplerChunk, sizeof(samplerChunk) ); + if ( LittleLong( samplerChunk.cSampleLoops ) > 0 ) + { + // only support normal forward loops + if ( LittleLong( samplerChunk.Loops[0].dwType ) == 0 ) + { + m_loopStart = LittleLong( samplerChunk.Loops[0].dwStart ); + } +#ifdef _DEBUG + else + { + Msg("Unknown sampler chunk type %d on %s\n", LittleLong( samplerChunk.Loops[0].dwType ), m_pSfx->GetFileName() ); + } +#endif + } + // else discard - this is some other non-loop sampler data we don't support +} + + +//----------------------------------------------------------------------------- +// Purpose: get the wave header +//----------------------------------------------------------------------------- +void *CAudioSourceWave::GetHeader( void ) +{ + return m_pHeader; +} + +//----------------------------------------------------------------------------- +// Gets the looping information. Some parameters are interpreted based on format +//----------------------------------------------------------------------------- +int CAudioSourceWave::GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples ) +{ + if ( pLoopBlock ) + { + // for xma, the block that contains the loop point + *pLoopBlock = m_loopBlock; + } + + if ( pNumLeadingSamples ) + { + // for xma, the number of leading samples at the loop block to discard + *pNumLeadingSamples = m_numLeadingSamples; + } + + if ( pNumTrailingSamples ) + { + // for xma, the number of trailing samples at the final block to discard + *pNumTrailingSamples = m_numTrailingSamples; + } + + // the loop point in samples + return m_loopStart; +} + +//----------------------------------------------------------------------------- +// Purpose: wrap the position wrt looping +// Input : samplePosition - absolute position +// Output : int - looped position +//----------------------------------------------------------------------------- +int CAudioSourceWave::ConvertLoopedPosition( int samplePosition ) +{ + if ( m_format == WAVE_FORMAT_XMA ) + { + // xma mixer interprets loops and *always* sends a corrected position + return samplePosition; + } + + // if the wave is looping and we're past the end of the sample + // convert to a position within the loop + // At the end of the loop, we return a short buffer, and subsequent call + // will loop back and get the rest of the buffer + if ( m_loopStart >= 0 && samplePosition >= m_sampleCount ) + { + // size of loop + int loopSize = m_sampleCount - m_loopStart; + // subtract off starting bit of the wave + samplePosition -= m_loopStart; + + if ( loopSize ) + { + // "real" position in memory (mod off extra loops) + samplePosition = m_loopStart + (samplePosition % loopSize); + } + // ERROR? if no loopSize + } + + return samplePosition; +} + +//----------------------------------------------------------------------------- +// Purpose: remove the reference for the mixer getting deleted +// Input : *pMixer - +//----------------------------------------------------------------------------- +void CAudioSourceWave::ReferenceRemove( CAudioMixer *pMixer ) +{ + m_refCount--; + + if ( m_refCount == 0 && ( ( IsPC() && IsPlayOnce() ) || ( IsX360() && IsStreaming() ) ) ) + { + SetPlayOnce( false ); // in case it gets used again + CacheUnload(); + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Add a mixer reference +// Input : *pMixer - +//----------------------------------------------------------------------------- +void CAudioSourceWave::ReferenceAdd( CAudioMixer *pMixer ) +{ + m_refCount++; +} + + +//----------------------------------------------------------------------------- +// Purpose: return true if no mixers reference this source +//----------------------------------------------------------------------------- +bool CAudioSourceWave::CanDelete( void ) +{ + if ( m_refCount > 0 ) + return false; + + return true; +} + + +// CAudioSourceMemWave is a bunch of wave data that is all in memory. +// To use it: +// - derive from CAudioSourceMemWave +// - call CAudioSourceWave::Init with a WAVEFORMATEX +// - set m_sampleCount. +// - implement GetDataPointer +class CAudioSourceMemWave : public CAudioSourceWave +{ +public: + CAudioSourceMemWave(); + CAudioSourceMemWave( CSfxTable *pSfx ); + CAudioSourceMemWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + virtual ~CAudioSourceMemWave(); + + // These are all implemented by CAudioSourceMemWave. + virtual CAudioMixer* CreateMixer( int initialStreamPosition = 0 ); + virtual int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + virtual int ZeroCrossingBefore( int sample ); + virtual int ZeroCrossingAfter( int sample ); + + virtual int GetCacheStatus( void ); + virtual void CacheLoad( void ); + virtual void CacheUnload( void ); + + // by definition, should already be in memory + virtual void Prefetch() {} + + virtual void ParseChunk( IterateRIFF &walk, int chunkName ); + void ParseDataChunk( IterateRIFF &walk ); + +protected: + + // Whoeover derives must implement this. + virtual char *GetDataPointer( void ); + + memhandle_t m_hCache; + StreamHandle_t m_hStream; + +private: + CAudioSourceMemWave( const CAudioSourceMemWave & ); // not implemented, not accessible +}; + + +CAudioSourceMemWave::CAudioSourceMemWave() : + CAudioSourceWave( NULL ) +{ + m_hCache = 0; + m_hStream = INVALID_STREAM_HANDLE; +} + +CAudioSourceMemWave::CAudioSourceMemWave( CSfxTable *pSfx ) : + CAudioSourceWave( pSfx ) +{ + m_hCache = 0; + m_hStream = INVALID_STREAM_HANDLE; + + if ( IsX360() ) + { + bool bValid = GetXboxAudioStartupData(); + if ( !bValid ) + { + // failed, substitute placeholder + pSfx->m_bUseErrorFilename = true; + bValid = GetXboxAudioStartupData(); + if ( bValid ) + { + DevWarning( "Failed to load sound \"%s\", substituting \"%s\"\n", pSfx->getname(), pSfx->GetFileName() ); + } + } + + if ( bValid ) + { + // a 360 memory wave is a critical resource kept locked in memory, load its data now + CacheLoad(); + } + } +} + +CAudioSourceMemWave::CAudioSourceMemWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : + CAudioSourceWave( pSfx, info ) +{ + m_hCache = 0; + m_hStream = INVALID_STREAM_HANDLE; +} + +CAudioSourceMemWave::~CAudioSourceMemWave() +{ +} + +//----------------------------------------------------------------------------- +// Purpose: Creates a mixer and initializes it with an appropriate mixer +//----------------------------------------------------------------------------- +CAudioMixer *CAudioSourceMemWave::CreateMixer( int initialStreamPosition ) +{ + CAudioMixer *pMixer = CreateWaveMixer( CreateWaveDataMemory(*this), m_format, m_channels, m_bits, initialStreamPosition ); + + return pMixer; +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Input : **pData - output pointer to samples +// samplePosition - position (in samples not bytes) +// sampleCount - number of samples (not bytes) +// Output : int - number of samples available +//----------------------------------------------------------------------------- +int CAudioSourceMemWave::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + // handle position looping + samplePosition = ConvertLoopedPosition( samplePosition ); + + // how many samples are available (linearly not counting looping) + int totalSampleCount = m_sampleCount - samplePosition; + + // may be asking for a sample out of range, clip at zero + if ( totalSampleCount < 0 ) + { + totalSampleCount = 0; + } + + // clip max output samples to max available + if ( sampleCount > totalSampleCount ) + { + sampleCount = totalSampleCount; + } + + // byte offset in sample database + samplePosition *= m_sampleSize; + + // if we are returning some samples, store the pointer + if ( sampleCount ) + { + // Starting past end of "preloaded" data, just use regular cache + if ( samplePosition >= m_nCachedDataSize ) + { + *pData = GetDataPointer(); + } + else + { + if ( IsPC() || !IsX360() ) + { + // Start async loader if we haven't already done so + CacheLoad(); + + // Return less data if we are about to run out of uncached data + if ( samplePosition + ( sampleCount * m_sampleSize ) >= m_nCachedDataSize ) + { + sampleCount = ( m_nCachedDataSize - samplePosition ) / m_sampleSize; + } + + // Point at preloaded/cached data from .cache file for now + *pData = GetCachedDataPointer(); + } + else + { + // for 360, memory wave data should have already been loaded and locked in cache + Assert( 0 ); + } + } + + if ( *pData ) + { + *pData = (char *)*pData + samplePosition; + } + else + { + // End of data or some other problem + sampleCount = 0; + } + } + + return sampleCount; +} + + +// Hardcoded macros to test for zero crossing +#define ZERO_X_8(b) ((b)<2 && (b)>-2) +#define ZERO_X_16(b) ((b)<512 && (b)>-512) + +//----------------------------------------------------------------------------- +// Purpose: Search backward for a zero crossing starting at sample +// Input : sample - starting point +// Output : position of zero crossing +//----------------------------------------------------------------------------- +int CAudioSourceMemWave::ZeroCrossingBefore( int sample ) +{ + char *pWaveData = GetDataPointer(); + + if ( m_format == WAVE_FORMAT_PCM ) + { + if ( m_bits == 8 ) + { + char *pData = pWaveData + sample * m_sampleSize; + bool zero = false; + + if ( m_channels == 1 ) + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_8(*pData) ) + zero = true; + else + { + sample--; + pData--; + } + } + } + else + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_8(*pData) && ZERO_X_8(pData[1]) ) + zero = true; + else + { + sample--; + pData--; + } + } + } + } + else + { + short *pData = (short *)(pWaveData + sample * m_sampleSize); + bool zero = false; + + if ( m_channels == 1 ) + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_16(*pData) ) + zero = true; + else + { + pData--; + sample--; + } + } + } + else + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_16(*pData) && ZERO_X_16(pData[1]) ) + zero = true; + else + { + sample--; + pData--; + } + } + } + } + } + return sample; +} + + +//----------------------------------------------------------------------------- +// Purpose: Search forward for a zero crossing +// Input : sample - starting point +// Output : position of found zero crossing +//----------------------------------------------------------------------------- +int CAudioSourceMemWave::ZeroCrossingAfter( int sample ) +{ + char *pWaveData = GetDataPointer(); + + if ( m_format == WAVE_FORMAT_PCM ) + { + if ( m_bits == 8 ) + { + char *pData = pWaveData + sample * m_sampleSize; + bool zero = false; + + if ( m_channels == 1 ) + { + while ( sample < SampleCount() && !zero ) + { + if ( ZERO_X_8(*pData) ) + zero = true; + else + { + sample++; + pData++; + } + } + } + else + { + while ( sample < SampleCount() && !zero ) + { + if ( ZERO_X_8(*pData) && ZERO_X_8(pData[1]) ) + zero = true; + else + { + sample++; + pData++; + } + } + } + } + else + { + short *pData = (short *)(pWaveData + sample * m_sampleSize); + bool zero = false; + + if ( m_channels == 1 ) + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_16(*pData) ) + zero = true; + else + { + pData++; + sample++; + } + } + } + else + { + while ( sample > 0 && !zero ) + { + if ( ZERO_X_16(*pData) && ZERO_X_16(pData[1]) ) + zero = true; + else + { + sample++; + pData++; + } + } + } + } + } + return sample; +} + +//----------------------------------------------------------------------------- +// Purpose: parse chunks with unique processing to in-memory waves +// Input : &walk - RIFF file +//----------------------------------------------------------------------------- +void CAudioSourceMemWave::ParseChunk( IterateRIFF &walk, int chunkName ) +{ + switch( chunkName ) + { + // this is the audio data + case WAVE_DATA: + ParseDataChunk( walk ); + return; + } + + CAudioSourceWave::ParseChunk( walk, chunkName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: reads the actual sample data and parses it +// Input : &walk - RIFF file +//----------------------------------------------------------------------------- +void CAudioSourceMemWave::ParseDataChunk( IterateRIFF &walk ) +{ + m_dataStart = walk.ChunkFilePosition() + 8; + m_dataSize = walk.ChunkSize(); + + // 360 streaming model loads data later, but still needs critical member setup + char *pData = NULL; + if ( IsPC() || !IsX360() ) + { + pData = GetDataPointer(); + if ( !pData ) + { + Error( "CAudioSourceMemWave (%s): GetDataPointer() failed.", m_pSfx ? m_pSfx->GetFileName() : "m_pSfx = NULL" ); + } + + // load them into memory (bad!!, this is a duplicate read of the data chunk) + walk.ChunkRead( pData ); + } + + if ( m_format == WAVE_FORMAT_PCM ) + { + // number of samples loaded + m_sampleCount = m_dataSize / m_sampleSize; + m_numDecodedSamples = m_sampleCount; + } + else if ( m_format == WAVE_FORMAT_ADPCM ) + { + // The ADPCM mixers treat the wave source as a flat file of bytes. + // Since each "sample" is a byte (this is a flat file), the number of samples is the file size + m_sampleCount = m_dataSize; + m_sampleSize = 1; + + // file says 4, output is 16 + m_bits = 16; + + m_numDecodedSamples = ADPCMSampleCount( (ADPCMWAVEFORMAT *)m_pHeader, m_dataSize ); + } + + // some samples need to be converted + if ( pData ) + { + ConvertSamples( pData, m_sampleCount ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +int CAudioSourceMemWave::GetCacheStatus( void ) +{ + VPROF("CAudioSourceMemWave::GetCacheStatus"); + + if ( IsPC() || !IsX360() ) + { + // NOTE: This will start the load if it isn't started + bool bCacheValid; + bool bCompleted = wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ); + if ( !bCacheValid ) + { + wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(), m_dataSize, m_dataStart ); + } + if ( bCompleted ) + return AUDIO_IS_LOADED; + if ( wavedatacache->IsDataLoadInProgress( m_hCache ) ) + return AUDIO_LOADING; + } + else + { + return wavedatacache->IsStreamedDataReady( m_hStream ) ? AUDIO_IS_LOADED : AUDIO_NOT_LOADED; + } + + return AUDIO_NOT_LOADED; +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceMemWave::CacheLoad( void ) +{ + if ( IsPC() || !IsX360() ) + { + // Commence lazy load? + if ( m_hCache != 0 ) + { + bool bCacheValid; + wavedatacache->IsDataLoadCompleted( m_hCache, &bCacheValid ); + if ( !bCacheValid ) + { + wavedatacache->RestartDataLoad( &m_hCache, m_pSfx->GetFileName(), m_dataSize, m_dataStart ); + } + return; + } + + m_hCache = wavedatacache->AsyncLoadCache( m_pSfx->GetFileName(), m_dataSize, m_dataStart ); + } + else + { + if ( m_hStream == INVALID_STREAM_HANDLE ) + { + // memory wave is resident + const char *pFilename = m_pSfx->GetFileName(); + streamFlags_t streamFlags = STREAMED_FROMDVD; + char szFilename[MAX_PATH]; + if ( m_format == WAVE_FORMAT_XMA || m_format == WAVE_FORMAT_PCM ) + { + V_strcpy_safe( szFilename, pFilename ); + V_SetExtension( szFilename, ".360.wav", sizeof( szFilename ) ); + pFilename = szFilename; + + // memory resident xma waves use the queued loader + // restricting to XMA due to not correctly running a post ConvertSamples, which is not an issue for XMA + if ( g_pQueuedLoader->IsMapLoading() ) + { + // hint the wave data cache + streamFlags |= STREAMED_QUEUEDLOAD; + } + } + + // open stream to load as a single monolithic buffer + m_hStream = wavedatacache->OpenStreamedLoad( pFilename, m_dataSize, m_dataStart, 0, -1, m_dataSize, 1, streamFlags ); + if ( m_hStream != INVALID_STREAM_HANDLE && !( streamFlags & STREAMED_QUEUEDLOAD ) ) + { + // block and finish load, convert data once right now + char *pWaveData = (char *)wavedatacache->GetStreamedDataPointer( m_hStream, true ); + if ( pWaveData ) + { + ConvertSamples( pWaveData, m_dataSize/m_sampleSize ); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceMemWave::CacheUnload( void ) +{ + if ( IsPC() || !IsX360() ) + { + if ( m_hCache != 0 ) + { + wavedatacache->Unload( m_hCache ); + } + } + else + { + if ( m_hStream != INVALID_STREAM_HANDLE ) + { + wavedatacache->CloseStreamedLoad( m_hStream ); + m_hStream = INVALID_STREAM_HANDLE; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: +// Output : char +//----------------------------------------------------------------------------- +char *CAudioSourceMemWave::GetDataPointer( void ) +{ + char *pWaveData = NULL; + + if ( IsPC() || !IsX360() ) + { + bool bSamplesConverted = false; + + if ( m_hCache == 0 ) + { + // not in cache, start loading + CacheLoad(); + } + + // mount the requested data, blocks if necessary + wavedatacache->GetDataPointer( + m_hCache, + m_pSfx->GetFileName(), + m_dataSize, + m_dataStart, + (void **)&pWaveData, + 0, + &bSamplesConverted ); + + // If we have reloaded data from disk (async) and we haven't converted the samples yet, do it now + // FIXME: Is this correct for stereo wavs? + if ( pWaveData && !bSamplesConverted ) + { + ConvertSamples( pWaveData, m_dataSize/m_sampleSize ); + wavedatacache->SetPostProcessed( m_hCache, true ); + } + } + else + { + if ( m_hStream != INVALID_STREAM_HANDLE ) + { + // expected to be valid, unless failure during setup + pWaveData = (char *)wavedatacache->GetStreamedDataPointer( m_hStream, true ); + } + } + + return pWaveData; +} + +//----------------------------------------------------------------------------- +// Purpose: Wave source for streaming wave files +// UNDONE: Handle looping +//----------------------------------------------------------------------------- +class CAudioSourceStreamWave : public CAudioSourceWave, public IWaveStreamSource +{ +public: + CAudioSourceStreamWave( CSfxTable *pSfx ); + CAudioSourceStreamWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + ~CAudioSourceStreamWave(); + + CAudioMixer *CreateMixer( int initialStreamPosition = 0 ); + int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + void ParseChunk( IterateRIFF &walk, int chunkName ); + bool IsStreaming( void ) { return true; } + + virtual int GetCacheStatus( void ); + + // IWaveStreamSource + virtual int UpdateLoopingSamplePosition( int samplePosition ) + { + return ConvertLoopedPosition( samplePosition ); + } + virtual void UpdateSamples( char *pData, int sampleCount ) + { + ConvertSamples( pData, sampleCount ); + } + virtual int GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples ) + { + return CAudioSourceWave::GetLoopingInfo( pLoopBlock, pNumLeadingSamples, pNumTrailingSamples ); + } + + virtual void Prefetch(); + + virtual int SampleToStreamPosition( int samplePosition ); + virtual int StreamToSamplePosition( int streamPosition ); + +private: + CAudioSourceStreamWave( const CAudioSourceStreamWave & ); // not implemented, not accessible +}; + +//----------------------------------------------------------------------------- +// Purpose: Save a copy of the file name for instances to open later +// Input : *pFileName - filename +//----------------------------------------------------------------------------- +CAudioSourceStreamWave::CAudioSourceStreamWave( CSfxTable *pSfx ) : CAudioSourceWave( pSfx ) +{ + m_pSfx = pSfx; + m_dataStart = -1; + m_dataSize = 0; + m_sampleCount = 0; + + if ( IsX360() ) + { + bool bValid = GetXboxAudioStartupData(); + if ( !bValid ) + { + // failed, substitute placeholder + pSfx->m_bUseErrorFilename = true; + bValid = GetXboxAudioStartupData(); + if ( bValid ) + { + DevWarning( "Failed to load sound \"%s\", substituting \"%s\"\n", pSfx->getname(), pSfx->GetFileName() ); + } + } + } +} + +CAudioSourceStreamWave::CAudioSourceStreamWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ) : + CAudioSourceWave( pSfx, info ) +{ + m_pSfx = pSfx; + m_dataStart = info->DataStart(); + m_dataSize = info->DataSize(); + + m_sampleCount = info->SampleCount(); +} + +//----------------------------------------------------------------------------- +// Purpose: free the filename buffer +//----------------------------------------------------------------------------- +CAudioSourceStreamWave::~CAudioSourceStreamWave( void ) +{ +} + + +//----------------------------------------------------------------------------- +// Purpose: Create an instance (mixer & wavedata) of this sound +// Output : CAudioMixer * - pointer to the mixer +//----------------------------------------------------------------------------- +CAudioMixer *CAudioSourceStreamWave::CreateMixer( int initialStreamPosition ) +{ + char fileName[MAX_PATH]; + const char *pFileName = m_pSfx->GetFileName(); + if ( IsX360() && ( m_format == WAVE_FORMAT_XMA || m_format == WAVE_FORMAT_PCM ) ) + { + V_strcpy_safe( fileName, pFileName ); + V_SetExtension( fileName, ".360.wav", sizeof( fileName ) ); + pFileName = fileName; + + // for safety, validate the initial stream position + // not trusting save/load + if ( m_format == WAVE_FORMAT_XMA ) + { + if ( ( initialStreamPosition % XBOX_DVD_SECTORSIZE ) || + ( initialStreamPosition % XMA_BLOCK_SIZE ) || + ( initialStreamPosition >= m_dataSize ) ) + { + initialStreamPosition = 0; + } + } + } + + // BUGBUG: Source constructs the IWaveData, mixer frees it, fix this? + IWaveData *pWaveData = CreateWaveDataStream( *this, static_cast<IWaveStreamSource *>(this), pFileName, m_dataStart, m_dataSize, m_pSfx, initialStreamPosition ); + if ( pWaveData ) + { + CAudioMixer *pMixer = CreateWaveMixer( pWaveData, m_format, m_channels, m_bits, initialStreamPosition ); + if ( pMixer ) + { + return pMixer; + } + + // no mixer, delete the stream buffer/instance + delete pWaveData; + } + + return NULL; +} + +void CAudioSourceStreamWave::Prefetch() +{ + PrefetchDataStream( m_pSfx->GetFileName(), m_dataStart, m_dataSize ); +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CAudioSourceStreamWave::SampleToStreamPosition( int samplePosition ) +{ + if ( IsPC() ) + { + // not for PC + Assert( 0 ); + return 0; + } + + if ( m_format != WAVE_FORMAT_XMA || !m_nHeaderSize ) + { + // not in the expected format or lacking the seek table + return 0; + } + + // Run through the seek table to find the block closest to the desired sample. + // Each seek table entry is the index (counting from the beginning of the file) + // of the first sample in the corresponding block, but there's no entry for the + // first block (since the index would always be zero). + int *pSeekTable = (int*)m_pHeader; + int packet = 0; + for ( int i = 0; i < m_nHeaderSize/(int)sizeof( int ); ++i ) + { + if ( samplePosition < pSeekTable[i] ) + { + packet = i; + break; + } + } + + int streamPosition = ( packet == 0 ) ? 0 : ( packet - 1 ) * 2048; + return streamPosition; +} + +//----------------------------------------------------------------------------- +//----------------------------------------------------------------------------- +int CAudioSourceStreamWave::StreamToSamplePosition( int streamPosition ) +{ + if ( IsPC() ) + { + // not for PC + Assert( 0 ); + return 0; + } + + if ( m_format != WAVE_FORMAT_XMA || !m_nHeaderSize ) + { + // not in the expected format or lacking the seek table + return 0; + } + + int packet = streamPosition/2048; + if ( packet <= 0 ) + { + return 0; + } + if ( packet > m_nHeaderSize/(int)sizeof( int ) ) + { + return m_numDecodedSamples; + } + + return ((int*)m_pHeader)[packet - 1]; +} + +//----------------------------------------------------------------------------- +// Purpose: Parse a stream wave file chunk +// unlike the in-memory file, don't load the data, just get a reference to it. +// Input : &walk - RIFF file +//----------------------------------------------------------------------------- +void CAudioSourceStreamWave::ParseChunk( IterateRIFF &walk, int chunkName ) +{ + // NOTE: It would be nice to break out of parsing once we have the data start and + // save seeking over the whole file. But to do so, the other needed chunks must occur + // before the DATA chunk. But, that is not standard and breaks most other wav parsers. + + switch( chunkName ) + { + case WAVE_DATA: + // data starts at chunk + 8 (chunk name, chunk size = 2*4=8 bytes) + // don't load the data, just know where it is so each instance + // can load it later + m_dataStart = walk.ChunkFilePosition() + 8; + m_dataSize = walk.ChunkSize(); + m_sampleCount = m_dataSize / m_sampleSize; + return; + } + CAudioSourceWave::ParseChunk( walk, chunkName ); +} + +//----------------------------------------------------------------------------- +// Purpose: This is not implemented here. This source has no data. It is the +// WaveData's responsibility to load/serve the data +//----------------------------------------------------------------------------- +int CAudioSourceStreamWave::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + return 0; +} + +int CAudioSourceStreamWave::GetCacheStatus( void ) +{ + if ( !m_dataSize || !m_dataStart ) + { + // didn't get precached properly + return AUDIO_NOT_LOADED; + } + + return AUDIO_IS_LOADED; +} + + +//----------------------------------------------------------------------------- +// Purpose: Create a wave audio source (streaming or in memory) +// Input : *pName - file name (NOTE: CAUDIOSOURCE KEEPS A POINTER TO pSfx) +// streaming - if true, don't load, stream each instance +// Output : CAudioSource * - a new source +//----------------------------------------------------------------------------- +CAudioSource *CreateWave( CSfxTable *pSfx, bool bStreaming ) +{ + Assert( pSfx ); + +#if defined( _DEBUG ) + // For some reason you can't usually do pSfx->getname() in the dev studio debugger, so for convenience we'll grab the name + // here in debug builds at least... + char const *pName = pSfx->getname(); + NOTE_UNUSED( pName ); +#endif + + CAudioSourceWave *pWave = NULL; + + if ( IsPC() || !IsX360() ) + { + // Caching should always work, so if we failed to cache, it's a problem reading the file data, etc. + bool bIsMapSound = pSfx->IsPrecachedSound(); + CAudioSourceCachedInfo *pInfo = audiosourcecache->GetInfo( CAudioSource::AUDIO_SOURCE_WAV, bIsMapSound, pSfx ); + + if ( pInfo && pInfo->Type() != CAudioSource::AUDIO_SOURCE_UNK ) + { + // create the source from this file + if ( bStreaming ) + { + pWave = new CAudioSourceStreamWave( pSfx, pInfo ); + } + else + { + pWave = new CAudioSourceMemWave( pSfx, pInfo ); + } + } + } + else + { + // 360 does not use audio cache system + // create the desired type + if ( bStreaming ) + { + pWave = new CAudioSourceStreamWave( pSfx ); + } + else + { + pWave = new CAudioSourceMemWave( pSfx ); + } + } + + if ( pWave && !pWave->Format() ) + { + // lack of format indicates failure + delete pWave; + pWave = NULL; + } + + return pWave; +} + + +//----------------------------------------------------------------------------- +// Purpose: Wrapper for CreateWave() +//----------------------------------------------------------------------------- +CAudioSource *Audio_CreateStreamedWave( CSfxTable *pSfx ) +{ +#if defined( MP3_SUPPORT ) + if ( Audio_IsMP3( pSfx->GetFileName() ) ) + { + return Audio_CreateStreamedMP3( pSfx ); + } +#endif + + return CreateWave( pSfx, true ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Wrapper for CreateWave() +//----------------------------------------------------------------------------- +CAudioSource *Audio_CreateMemoryWave( CSfxTable *pSfx ) +{ +#if defined( MP3_SUPPORT ) + if ( Audio_IsMP3( pSfx->GetFileName() ) ) + { + return Audio_CreateMemoryMP3( pSfx ); + } +#endif + + return CreateWave( pSfx, false ); +} + +float GetMP3Duration_Helper( char const *filename ); +static float Audio_GetMP3Duration( char const *pName ) +{ + // Deduce from file + return GetMP3Duration_Helper( pName ); +} + + +void MaybeReportMissingWav( char const *wav ) +{ + static CUtlSymbolTable wavErrors; + + CUtlSymbol sym; + sym = wavErrors.Find( wav ); + if ( UTL_INVAL_SYMBOL == sym ) + { + // See if file exists + if ( g_pFullFileSystem->FileExists( wav ) ) + { + DevWarning( "Bad Audio file '%s'\n", wav ); + } + else + { + DevWarning( "Missing wav file '%s'\n", wav ); + } + wavErrors.AddString( wav ); + } +} + +static float Audio_GetWaveDuration( char const *pName ) +{ + if ( IsX360() ) + { + // should have precached + return 0; + } + + char formatBuffer[1024]; + WAVEFORMATEX *pfmt = (WAVEFORMATEX *)formatBuffer; + + InFileRIFF riff( pName, *g_pSndIO ); + + if ( riff.RIFFName() != RIFF_WAVE ) + { + MaybeReportMissingWav( pName ); + return 0.0f; + } + + // set up the iterator for the whole file (root RIFF is a chunk) + IterateRIFF walk( riff, riff.RIFFSize() ); + + int format = 0; + int formatSize = 0; + int sampleCount = 0; + + // This chunk must be first as it contains the wave's format + // break out when we've parsed it + while ( walk.ChunkAvailable() && ( format == 0 || sampleCount == 0 ) ) + { + switch( walk.ChunkName() ) + { + case WAVE_FMT: + if ( walk.ChunkSize() <= sizeof( formatBuffer ) ) + { + walk.ChunkRead( formatBuffer ); + formatSize = walk.ChunkSize(); + format = LittleWord( pfmt->wFormatTag ); + } + break; + case WAVE_DATA: + if ( format != 0 ) + { + int dataSize = walk.ChunkSize(); + if ( format == WAVE_FORMAT_ADPCM ) + { + // Dummy size for now + sampleCount = dataSize; + } + else + { + sampleCount = dataSize / ( LittleWord( pfmt->wBitsPerSample ) >> 3 ); + } + } + break; + default: + ChunkError( walk.ChunkName() ); + break; + } + walk.ChunkNext(); + } + + // Not really a WAVE file or no format chunk, bail + if ( !format || !sampleCount ) + return 0.0f; + + float sampleRate = LittleDWord( pfmt->nSamplesPerSec ); + + if ( format == WAVE_FORMAT_ADPCM ) + { + // Determine actual duration + sampleCount = ADPCMSampleCount( (ADPCMWAVEFORMAT *)formatBuffer, sampleCount ); + } + + return (float)sampleCount / sampleRate; +} + +//----------------------------------------------------------------------------- +// Purpose: Fast method for determining duration of .wav/.mp3, exposed to server as well +// Input : *pName - +// Output : float +//----------------------------------------------------------------------------- +float AudioSource_GetSoundDuration( char const *pName ) +{ +#if defined( MP3_SUPPORT ) + if ( Audio_IsMP3( pName ) ) + { + return Audio_GetMP3Duration( pName ); + } +#endif + + CSfxTable *pSound = S_PrecacheSound( pName ); + if ( pSound ) + { + return AudioSource_GetSoundDuration( pSound ); + } + + return Audio_GetWaveDuration( pName ); +} + +float AudioSource_GetSoundDuration( CSfxTable *pSfx ) +{ + if ( pSfx && pSfx->pSource ) + { + return (float)pSfx->pSource->SampleCount() / (float)pSfx->pSource->SampleRate(); + } + + return 0; +} + +CAudioSourceCachedInfo::CAudioSourceCachedInfo() : + infolong( 0 ), + flagsbyte( 0 ), + m_dataStart( 0 ), + m_dataSize( 0 ), + m_loopStart( 0 ), + m_sampleCount( 0 ), + m_usCachedDataSize( 0 ), + m_pCachedData( 0 ), + m_usHeaderSize( 0 ), + m_pHeader( 0 ), + m_pSentence( 0 ) +{ +} + +CAudioSourceCachedInfo& CAudioSourceCachedInfo::operator =( const CAudioSourceCachedInfo& src ) +{ + if ( this == &src ) + return *this; + + infolong = src.infolong; + flagsbyte = src.flagsbyte; + SetDataStart( src.DataStart() ); + SetDataSize( src.DataSize() ); + SetLoopStart( src.LoopStart() ); + SetSampleCount( src.SampleCount() ); + + CSentence *scopy = NULL; + if ( src.Sentence() ) + { + scopy = new CSentence(); + *scopy = *src.Sentence(); + } + SetSentence( scopy ); + + byte *data = NULL; + + Assert( src.CachedDataSize() == 0 || src.CachedData() ); + + m_usCachedDataSize = 0; + + if ( src.CachedData() && src.CachedDataSize() > 0 ) + { + SetCachedDataSize( src.CachedDataSize() ); + data = new byte[ src.CachedDataSize() ]; + Assert( data ); + Q_memcpy( data, src.CachedData(), src.CachedDataSize() ); + } + + SetCachedData( data ); + + data = NULL; + + Assert( src.HeaderSize() == 0 || src.HeaderData() ); + + m_usHeaderSize = 0; + + if ( src.HeaderData() && src.HeaderSize() > 0 ) + { + SetHeaderSize( src.HeaderSize() ); + data = new byte[ src.HeaderSize() ]; + Assert( data ); + Q_memcpy( data, src.HeaderData(), src.HeaderSize() ); + } + + SetHeaderData( data ); + + return *this; +} + +CAudioSourceCachedInfo::CAudioSourceCachedInfo( const CAudioSourceCachedInfo& src ) +{ + if ( this == &src ) + { + Assert( 0 ); + return; + } + + infolong = src.infolong; + flagsbyte = src.flagsbyte; + SetDataStart( src.DataStart() ); + SetDataSize( src.DataSize() ); + SetLoopStart( src.LoopStart() ); + SetSampleCount( src.SampleCount() ); + + CSentence *scopy = NULL; + if ( src.Sentence() ) + { + scopy = new CSentence(); + *scopy = *src.Sentence(); + } + SetSentence( scopy ); + + byte *data = NULL; + + Assert( src.CachedDataSize() == 0 || src.CachedData() ); + + m_usCachedDataSize = 0; + + if ( src.CachedData() && src.CachedDataSize() > 0 ) + { + SetCachedDataSize( src.CachedDataSize() ); + data = new byte[ src.CachedDataSize() ]; + Assert( data ); + Q_memcpy( data, src.CachedData(), src.CachedDataSize() ); + } + + SetCachedData( data ); + + data = NULL; + + Assert( src.HeaderSize() == 0 || src.HeaderData() ); + + m_usHeaderSize = 0; + + if ( src.HeaderData() && src.HeaderSize() > 0 ) + { + SetHeaderSize( src.HeaderSize() ); + data = new byte[ src.HeaderSize() ]; + Assert( data ); + Q_memcpy( data, src.HeaderData(), src.HeaderSize() ); + } + + SetHeaderData( data ); +} + +CAudioSourceCachedInfo::~CAudioSourceCachedInfo() +{ + Clear(); +} + +void CAudioSourceCachedInfo::Clear() +{ + infolong = 0; + flagsbyte = 0; + m_dataStart = 0; + m_dataSize = 0; + m_loopStart = 0; + m_sampleCount = 0; + + delete m_pSentence; + m_pSentence = NULL; + + delete[] m_pCachedData; + m_pCachedData = NULL; + m_usCachedDataSize = 0; + + delete[] m_pHeader; + m_pHeader = NULL; + m_usHeaderSize = 0; +} + +void CAudioSourceCachedInfo::RemoveData() +{ + delete[] m_pCachedData; + m_pCachedData = NULL; + m_usCachedDataSize = 0; + flags.m_bCachedData = false; +} + +void CAudioSourceCachedInfo::Save( CUtlBuffer& buf ) +{ + buf.PutInt( infolong ); + buf.PutChar( flagsbyte ); + buf.PutInt( m_dataStart ); + buf.PutInt( m_dataSize ); + buf.PutInt( m_loopStart ); + buf.PutInt( m_sampleCount ); + + if ( flags.m_bSentence ) + { + m_pSentence->CacheSaveToBuffer( buf, CACHED_SENTENCE_VERSION ); + } + + Assert( m_usCachedDataSize < 65535 ); + + if ( flags.m_bCachedData && m_pCachedData ) + { + buf.PutInt( m_usCachedDataSize ); + buf.Put( m_pCachedData, m_usCachedDataSize ); + } + + Assert( m_usHeaderSize <= 32767 ); + + if ( flags.m_bHeader ) + { + buf.PutShort( m_usHeaderSize ); + buf.Put( m_pHeader, m_usHeaderSize ); + } +} + +void CAudioSourceCachedInfo::Restore( CUtlBuffer& buf ) +{ + // Wipe any old data!!! + Clear(); + + infolong = buf.GetInt(); + flagsbyte = buf.GetChar(); + m_dataStart = buf.GetInt(); + m_dataSize = buf.GetInt(); + m_loopStart = buf.GetInt(); + m_sampleCount = buf.GetInt(); + if ( flags.m_bSentence ) + { + m_pSentence = new CSentence(); + Assert( m_pSentence ); + m_pSentence->CacheRestoreFromBuffer( buf ); + } + + if ( flags.m_bCachedData ) + { + m_usCachedDataSize = buf.GetInt(); + Assert( m_usCachedDataSize > 0 && m_usCachedDataSize < 65535 ); + if ( m_usCachedDataSize > 0 ) + { + byte *data = new byte[ m_usCachedDataSize ]; + buf.Get( data, m_usCachedDataSize ); + SetCachedData( data ); + } + } + + if ( flags.m_bHeader ) + { + m_usHeaderSize = buf.GetShort(); + Assert( m_usHeaderSize > 0 && m_usHeaderSize <= 32767 ); + if ( m_usHeaderSize > 0 ) + { + byte *data = new byte[ m_usHeaderSize ]; + buf.Get( data, m_usHeaderSize ); + SetHeaderData( data ); + } + } +} + +int CAudioSourceCachedInfo::s_CurrentType = CAudioSource::AUDIO_SOURCE_MAXTYPE; +CSfxTable *CAudioSourceCachedInfo::s_pSfx = NULL; +bool CAudioSourceCachedInfo::s_bIsPrecacheSound = false; + +void CAudioSourceCachedInfo::Rebuild( char const *filename ) +{ + // Wipe any old data + Clear(); + + Assert( s_pSfx ); + Assert( s_CurrentType != CAudioSource::AUDIO_SOURCE_MAXTYPE ); + +#if 0 + // Never cachify something which is not in the client precache list + if ( s_bIsPrecacheSound != s_pSfx->IsPrecachedSound() ) + { + Msg( "Logic bug, precaching entry for '%s' which is not in precache list\n", + filename ); + } +#endif + + SetType( s_CurrentType ); + + CAudioSource *as = NULL; + + // Note though these instantiate a specific AudioSource subclass, it doesn't matter, we just need one for .wav and one for .mp3 + switch ( s_CurrentType ) + { + default: + case CAudioSource::AUDIO_SOURCE_VOICE: + break; + case CAudioSource::AUDIO_SOURCE_WAV: + as = new CAudioSourceMemWave( s_pSfx ); + break; + case CAudioSource::AUDIO_SOURCE_MP3: +#if defined( MP3_SUPPORT ) + as = new CAudioSourceMP3Cache( s_pSfx ); +#endif + break; + } + + if ( as ) + { + as->GetCacheData( this ); + delete as; + } +} + +// Versions +// 3: The before time +// 4: Changed MP3 caching to ensure we store proper sample rate, removed hack to not cache vo/ +// 5: Fixed bug that could result in incorrect mp3 datasizes in the sound cache +#define AUDIOSOURCE_CACHE_VERSION 5 +class CAudioSourceCache : public IAudioSourceCache +{ +public: + + struct SearchPathCache : CUtlCachedFileData< CAudioSourceCachedInfo > + { + SearchPathCache( const char *pszRepositoryFilename, const char *pszSearchPath, UtlCachedFileDataType_t eOutOfDateMethod ) + : CUtlCachedFileData( pszRepositoryFilename, AUDIOSOURCE_CACHE_VERSION, AsyncLookaheadMetaChecksum, eOutOfDateMethod ) + { + V_strcpy_safe( m_szSearchPath, pszSearchPath ); + + // Delete any existing cache if it's out of date + IsUpToDate(); + + // Load up existing cache file + Init(); + } + + char m_szSearchPath[ MAX_PATH ]; + + virtual ~SearchPathCache() + { + Shutdown(); + } + }; + + + CAudioSourceCache() + { + m_nServerCount = -1; + m_bSndCacheDebug = false; + } + + bool Init( unsigned int memSize ); + void Shutdown(); + + void CheckSaveDirtyCaches(); + void CheckCacheBuild(); + void BuildCache( char const *pszSearchPath ); + + static unsigned int AsyncLookaheadMetaChecksum( void ); + + void LevelInit( char const *mapname ); + void LevelShutdown(); + + virtual CAudioSourceCachedInfo *GetInfo( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ); + virtual void RebuildCacheEntry( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ); + virtual void ForceRecheckDiskInfo(); + +private: + SearchPathCache *LookUpCacheEntry( const char *szCleanedFilename, int audiosourcetype, bool soundisprecached, CSfxTable *sfx ); + + SearchPathCache *FindCacheForSearchPath( const char *pszSearchPath ); + SearchPathCache *CreateCacheForSearchPath( const char *pszSearchPath ); + + static void GetSoundFilename( char *szResult, int nResultSize, const char *pszInputFilename ); + + void RemoveCache( char const *cachename ); + + // List of all loaded caches + CUtlVector<SearchPathCache*> m_vecCaches; + + int m_nServerCount; + bool m_bSndCacheDebug; +}; + +static CAudioSourceCache g_ASCache; +IAudioSourceCache *audiosourcecache = &g_ASCache; + +unsigned int CAudioSourceCachedInfoHandle_t::s_nCurrentFlushCount = 1; + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceCachedInfoHandle_t::InvalidateCache() +{ + ++s_nCurrentFlushCount; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Output : Returns true on success, false on failure. +//----------------------------------------------------------------------------- +bool CAudioSourceCache::Init( unsigned int memSize ) +{ +#if defined( _DEBUG ) + Msg( "CAudioSourceCache: Init\n" ); +#endif + + m_bSndCacheDebug = CommandLine()->FindParm( "-sndcachedebug" ) ? true : false; + + if ( !wavedatacache->Init( memSize ) ) + { + Error( "Unable to init wavedatacache system\n" ); + return false; + } + + if ( IsX360() ) + { + // 360 doesn't use audio source caches + return true; + } + + // Gather up list of search paths + CUtlVector< CUtlString > vecSearchPaths; + GetSearchPath( vecSearchPaths, "game" ); + + // Create corresponding caches + FOR_EACH_VEC( vecSearchPaths, idxSearchPath ) + { + + // Standardize the name + char szSearchPath[ MAX_PATH ]; + V_strcpy_safe( szSearchPath, vecSearchPaths[idxSearchPath] ); + V_FixSlashes( szSearchPath ); + V_AppendSlash( szSearchPath, sizeof(szSearchPath ) ); + + // See if we already have a cache for this search path. + bool bFound = false; + FOR_EACH_VEC( m_vecCaches, idxCache ) + { + if ( V_stricmp( szSearchPath, m_vecCaches[idxCache]->m_szSearchPath ) == 0 ) + { + Assert( V_strcmp( szSearchPath, m_vecCaches[idxCache]->m_szSearchPath ) == 0 ); // case *should* match exactly + bFound = true; + break; + } + } + if ( bFound ) + continue; + + // Add a ceche + SearchPathCache *pCache = CreateCacheForSearchPath( szSearchPath ); + m_vecCaches.AddToTail( pCache ); + } + + return true; +} + +CAudioSourceCache::SearchPathCache *CAudioSourceCache::FindCacheForSearchPath( const char *pszSearchPath ) +{ + FOR_EACH_VEC( m_vecCaches, idx ) + { + SearchPathCache *pCache = m_vecCaches[idx]; + if ( V_stricmp( pCache->m_szSearchPath, pszSearchPath ) == 0 ) + { + return pCache; + } + } + + return NULL; +} + +CAudioSourceCache::SearchPathCache *CAudioSourceCache::CreateCacheForSearchPath( const char *pszSearchPath ) +{ + + // Make sure search path ends in a slash + char szSearchPath[ MAX_PATH ]; + V_strcpy_safe( szSearchPath, pszSearchPath ); + V_AppendSlash( szSearchPath, sizeof(szSearchPath) ); + + // Set the filename for the cache. + UtlCachedFileDataType_t eOutOfDateMethod = UTL_CACHED_FILE_USE_FILESIZE; + char szCacheName[ MAX_PATH + 32 ]; + V_strcpy_safe( szCacheName, szSearchPath ); + char *dotVpkSlash = V_stristr( szCacheName, ".vpk" CORRECT_PATH_SEPARATOR_S ); + if ( dotVpkSlash ) + { + Assert( dotVpkSlash[5] == '\0' ); + char *d = dotVpkSlash+4; // backup to where the slash is + Assert( *d == CORRECT_PATH_SEPARATOR ); + V_strcpy( d, ".sound.cache" ); + } + else + { + V_strcat_safe( szCacheName, "sound" CORRECT_PATH_SEPARATOR_S "sound.cache" ); + eOutOfDateMethod = UTL_CACHED_FILE_USE_TIMESTAMP; + } + + return new SearchPathCache( szCacheName, szSearchPath, eOutOfDateMethod ); +} + +//----------------------------------------------------------------------------- +void CAudioSourceCache::Shutdown() +{ +#if defined( _DEBUG ) + Msg( "CAudioSourceCache: Shutdown\n" ); +#endif + + CheckSaveDirtyCaches(); + m_vecCaches.PurgeAndDeleteElements(); + + wavedatacache->Shutdown(); +} + +//----------------------------------------------------------------------------- +// Purpose: Called by Host_Init on engine startup to rebuild everything if needed +//----------------------------------------------------------------------------- +void CAudioSourceCache::CheckCacheBuild() +{ + if ( IsX360() ) + { + return; + } + + // !FIXME! We'll just do everything lazily for now! + FOR_EACH_VEC( m_vecCaches, idx ) + { + } +} + +//----------------------------------------------------------------------------- +void CAudioSourceCache::CheckSaveDirtyCaches() +{ + FOR_EACH_VEC( m_vecCaches, idx ) + { + SearchPathCache *pCache = m_vecCaches[idx]; + if ( pCache->IsDirty() && pCache->GetNumElements() > 0 ) + { + Msg( "Saving %s\n", pCache->GetRepositoryFileName() ); + pCache->Save(); + } + } +} + +//----------------------------------------------------------------------------- +// Purpose: Static method +// Output : unsigned int +//----------------------------------------------------------------------------- +unsigned int CAudioSourceCache::AsyncLookaheadMetaChecksum( void ) +{ + if ( IsX360() ) + { + return 0; + } + + CRC32_t crc; + CRC32_Init( &crc ); + + float f = SND_ASYNC_LOOKAHEAD_SECONDS; + CRC32_ProcessBuffer( &crc, &f, sizeof( f ) ); + // Finish + CRC32_Final( &crc ); + + return (unsigned int)crc; +} + +//----------------------------------------------------------------------------- +// Purpose: +// Input : *mapname - +//----------------------------------------------------------------------------- +void CAudioSourceCache::LevelInit( char const *mapname ) +{ + CheckSaveDirtyCaches(); +} + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +void CAudioSourceCache::LevelShutdown() +{ + CheckSaveDirtyCaches(); +} + +//----------------------------------------------------------------------------- +void CAudioSourceCache::GetSoundFilename( char *szResult, int nResultSize, const char *pszInputFilename ) +{ + V_snprintf( szResult, nResultSize, "sound/%s", pszInputFilename ); + V_FixSlashes( szResult ); + V_RemoveDotSlashes( szResult ); + V_strlower( szResult ); +} + +//----------------------------------------------------------------------------- +CAudioSourceCache::SearchPathCache *CAudioSourceCache::LookUpCacheEntry( const char *fn, int audiosourcetype, bool soundisprecached, CSfxTable *sfx ) +{ + if ( IsX360() ) + { + return NULL; + } + + // Hack to remember the type of audiosource to create if we need to recreate it + CAudioSourceCachedInfo::s_CurrentType = audiosourcetype; + CAudioSourceCachedInfo::s_pSfx = sfx; + CAudioSourceCachedInfo::s_bIsPrecacheSound = soundisprecached; + + // Get cleaned up filename + char szRelFilename[ 256 ]; + GetSoundFilename( szRelFilename, sizeof( szRelFilename ), sfx->GetFileName() ); + + // Get absolute filename. This thing had better exist in the filesystem somewhere + char szAbsFilename[ 1024 ]; + if ( !g_pFullFileSystem->RelativePathToFullPath( szRelFilename, "game", szAbsFilename, sizeof(szAbsFilename) ) ) + { + return NULL; + } + + // now try to figure out which search path this corresponds to + FOR_EACH_VEC( m_vecCaches, idx ) + { + SearchPathCache *pCache = m_vecCaches[idx]; + if ( V_strncmp( pCache->m_szSearchPath, szAbsFilename, V_strlen( pCache->m_szSearchPath ) ) == 0 ) + { + return pCache; + } + } + Warning( "Cannot figure out which search path %s came from. Absolute path is %s\n", szRelFilename, szAbsFilename ); + + SearchPathCache *pCache = NULL; + + return pCache; +} + + +//----------------------------------------------------------------------------- +// Purpose: +//----------------------------------------------------------------------------- +CAudioSourceCachedInfo *CAudioSourceCache::GetInfo( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ) +{ + VPROF("CAudioSourceCache::GetInfo"); + + if ( IsX360() ) + { + // 360 not using + return NULL; + } + + Assert( sfx ); + + char fn[ 512 ]; + GetSoundFilename( fn, sizeof( fn ), sfx->GetFileName() ); + + CAudioSourceCachedInfo *info = NULL; + SearchPathCache *pCache = LookUpCacheEntry( fn, audiosourcetype, soundisprecached, sfx ); + if ( !pCache ) + return NULL; + + info = pCache->Get( fn ); + +// Is this applicable anymore now that we have a cache per search path? +// if ( info && info->Format() == 0 ) +// { +// if ( g_pFullFileSystem->FileExists( fn, "BSP" ) ) +// { +// DevMsg( 1, "Forced rebuild of bsp cache sound '%s'\n", fn ); +// info = pCache->RebuildItem( fn ); +// Assert( info->Format() != 0 ); +// } +// } + + return info; +} + + +void CAudioSourceCache::RebuildCacheEntry( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ) +{ + VPROF("CAudioSourceCache::RebuildCacheEntry"); + + if ( IsX360() ) + { + // 360 not using + return; + } + + Assert( sfx ); + + char fn[ 512 ]; + GetSoundFilename( fn, sizeof( fn ), sfx->GetFileName() ); + SearchPathCache *pCache = LookUpCacheEntry( fn, audiosourcetype, soundisprecached, sfx ); + if ( !pCache ) + return; + + pCache->RebuildItem( fn ); +} + + +//----------------------------------------------------------------------------- +void CAudioSourceCache::ForceRecheckDiskInfo() +{ + FOR_EACH_VEC( m_vecCaches, idx ) + { + m_vecCaches[ idx ]->ForceRecheckDiskInfo(); + } +} + +//----------------------------------------------------------------------------- +void CAudioSourceCache::RemoveCache( char const *cachename ) +{ + if ( IsX360() ) + { + return; + } + + if ( g_pFullFileSystem->FileExists( cachename, "MOD" ) ) + { + if ( !g_pFullFileSystem->IsFileWritable( cachename, "MOD" ) ) + { + g_pFullFileSystem->SetFileWritable( cachename, true, "MOD" ); + } + g_pFullFileSystem->RemoveFile( cachename, "MOD" ); + } +} + +//----------------------------------------------------------------------------- +void CAudioSourceCache::BuildCache( char const *pszSearchPath ) +{ + + // Get absolute path + char szAbsPath[ MAX_PATH ]; + V_MakeAbsolutePath( szAbsPath, sizeof(szAbsPath), pszSearchPath ); + V_FixSlashes( szAbsPath ); + + // Add a search path to the filesystem. We'll add one search path as a kludge so we + // can use the existing file finder system easily + g_pFullFileSystem->AddSearchPath( szAbsPath, "soundcache_kludge", PATH_ADD_TO_HEAD ); + + Msg( "Finding .wav files...\n"); + CUtlVector< CUtlString > vecFilenames; + AddFilesToList( vecFilenames, "sound", "soundcache_kludge", "wav" ); + + Msg( "Finding .mp3 files...\n"); + AddFilesToList( vecFilenames, "sound", "soundcache_kludge", "mp3" ); + + Msg( "Found %d audio files.\n", vecFilenames.Count() ); + + g_pFullFileSystem->RemoveSearchPaths( "soundcache_kludge" ); + + if ( vecFilenames.Count() < 1 ) + { + Warning(" No audio files found. Not building cache\n" ); + return; + } + + // FindCacheForSearchPath expects an absolute search path, but if we're working with a VPK we'll have the path to + // the file, wherein the proper path to the file is /foo/bar.vpk, but the *search path* should be /foo/bar.vpk/ + char szAsSearchPath[MAX_PATH] = { 0 }; + V_strncpy( szAsSearchPath, szAbsPath, sizeof( szAsSearchPath ) ); + V_AppendSlash( szAsSearchPath, sizeof( szAsSearchPath ) ); + + SearchPathCache *pCache = FindCacheForSearchPath( szAsSearchPath ); + if ( !pCache ) + { + // This cache might not have existed on startup + pCache = CreateCacheForSearchPath( szAbsPath ); + m_vecCaches.AddToTail( pCache ); + } + + g_pFullFileSystem->AddSearchPath( szAbsPath, "game", PATH_ADD_TO_HEAD ); + int nLenAbsPath = V_strlen( szAbsPath ); + int iLastShownPct = -1; + FOR_EACH_VEC( vecFilenames, idxFilename ) + { + const char *pszFilename = vecFilenames[ idxFilename ]; + if ( V_strnicmp( pszFilename, szAbsPath, nLenAbsPath ) != 0 ) + { + Warning( "Sound %s doesn't begin with search path %s\n", pszFilename, szAbsPath ); + Assert( false ); + continue; + } + const char *pszRelName = pszFilename + nLenAbsPath; + if ( *pszRelName == '/' || *pszRelName == '\\' ) + ++pszRelName; + if ( V_strnicmp( pszRelName, "sound" CORRECT_PATH_SEPARATOR_S, 6 ) != 0 ) + { + Warning( "Relative name %s doesn't begin with leading 'sound' directory?\n", pszRelName ); + Assert( false ); + continue; + } + const char *pszName = pszRelName + 6; + + // Show progress + int iPct = idxFilename * 100 / vecFilenames.Count(); + if ( iPct != iLastShownPct ) + { + Msg( " %3d%% %s\n", iPct, pszName ); + iLastShownPct = iPct; + } + + CAudioSourceCachedInfo::s_bIsPrecacheSound = true; + CAudioSourceCachedInfo::s_CurrentType = CAudioSource::AUDIO_SOURCE_WAV; + char szExt[ 10 ] = { 0 }; + V_ExtractFileExtension( pszFilename, szExt, sizeof( szExt ) ); + if ( V_stricmp( szExt, "mp3" ) == 0 ) + { + CAudioSourceCachedInfo::s_CurrentType = CAudioSource::AUDIO_SOURCE_MP3; + } + CAudioSourceCachedInfo::s_pSfx = S_DummySfx( pszName ); + + const CAudioSourceCachedInfo *pInfo = pCache->Get( pszRelName ); + if ( !pInfo ) + { + Warning( "Failed to cache info for %s\n", pszFilename ); + } + } + g_pFullFileSystem->RemoveSearchPath( szAbsPath, "game" ); + + if ( pCache->IsDirty() ) + { + Msg( "Saving %s\n", pCache->GetRepositoryFileName() ); + pCache->Save(); + } + else + { + Msg( "No changes detected; not saving %s\n", pCache->GetRepositoryFileName() ); + } +} + +void CheckCacheBuild() +{ + g_ASCache.CheckCacheBuild(); +} + +CON_COMMAND( snd_buildcache, "<directory or VPK filename> Rebulds sound cache for a given search path.\n" ) +{ + if ( args.ArgC() < 2 ) + { + ConMsg( "Usage: snd_buildcache <directory or VPK filename>\n" ); + return; + } + + // Allow them to eitehr specify multiple args, or comma-seperated list. + // You cannot easily pas multiple args on the (OS) command line. + for ( int idxArg = 1 ; idxArg < args.ArgC() ; ++idxArg ) + { + CUtlStringList vecPaths; + V_SplitString( args[idxArg], ",", vecPaths ); + FOR_EACH_VEC( vecPaths, idxPath ) + { + g_ASCache.BuildCache( vecPaths[idxPath] ); + } + } + + // And now quit the game, because we mucked with search paths and the game is almost certainly not + // going to work anymore + Msg( "Quitting the game because we probably screwed up the search paths...\n" ); + extern void HostState_Shutdown(); + HostState_Shutdown(); +} + diff --git a/engine/audio/private/snd_wave_source.h b/engine/audio/private/snd_wave_source.h new file mode 100644 index 0000000..09fe4c1 --- /dev/null +++ b/engine/audio/private/snd_wave_source.h @@ -0,0 +1,160 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_WAVE_SOURCE_H +#define SND_WAVE_SOURCE_H +#pragma once + +#include "snd_audio_source.h" +class IterateRIFF; +#include "sentence.h" +#include "snd_sfx.h" + +//============================================================================= +// Functions to create audio sources from wave files or from wave data. +//============================================================================= +extern CAudioSource* Audio_CreateMemoryWave( CSfxTable *pSfx ); +extern CAudioSource* Audio_CreateStreamedWave( CSfxTable *pSfx ); + +class CAudioSourceWave : public CAudioSource +{ +public: + CAudioSourceWave( CSfxTable *pSfx ); + CAudioSourceWave( CSfxTable *pSfx, CAudioSourceCachedInfo *info ); + ~CAudioSourceWave( void ); + + virtual int GetType( void ); + virtual void GetCacheData( CAudioSourceCachedInfo *info ); + + void Setup( const char *pFormat, int formatSize, IterateRIFF &walk ); + virtual int SampleRate( void ); + virtual int SampleSize( void ); + virtual int SampleCount( void ); + + virtual int Format( void ); + virtual int DataSize( void ); + + void *GetHeader( void ); + virtual bool IsVoiceSource(); + + virtual void ParseChunk( IterateRIFF &walk, int chunkName ); + virtual void ParseSentence( IterateRIFF &walk ); + + void ConvertSamples( char *pData, int sampleCount ); + bool IsLooped( void ); + bool IsStereoWav( void ); + bool IsStreaming( void ); + int GetCacheStatus( void ); + int ConvertLoopedPosition( int samplePosition ); + void CacheLoad( void ); + void CacheUnload( void ); + virtual int ZeroCrossingBefore( int sample ); + virtual int ZeroCrossingAfter( int sample ); + virtual void ReferenceAdd( CAudioMixer *pMixer ); + virtual void ReferenceRemove( CAudioMixer *pMixer ); + virtual bool CanDelete( void ); + virtual CSentence *GetSentence( void ); + const char *GetName(); + + virtual bool IsAsyncLoad(); + + virtual void CheckAudioSourceCache(); + + virtual char const *GetFileName(); + + // 360 uses alternate play once semantics + virtual void SetPlayOnce( bool bIsPlayOnce ) { m_bIsPlayOnce = IsPC() ? bIsPlayOnce : false; } + virtual bool IsPlayOnce() { return IsPC() ? m_bIsPlayOnce : false; } + + virtual void SetSentenceWord( bool bIsWord ) { m_bIsSentenceWord = bIsWord; } + virtual bool IsSentenceWord() { return m_bIsSentenceWord; } + + int GetLoopingInfo( int *pLoopBlock, int *pNumLeadingSamples, int *pNumTrailingSamples ); + + virtual int SampleToStreamPosition( int samplePosition ) { return 0; } + virtual int StreamToSamplePosition( int streamPosition ) { return 0; } + +protected: + void ParseCueChunk( IterateRIFF &walk ); + void ParseSamplerChunk( IterateRIFF &walk ); + + void Init( const char *pHeaderBuffer, int headerSize ); + bool GetStartupData( void *dest, int destsize, int& bytesCopied ); + bool GetXboxAudioStartupData(); + + //----------------------------------------------------------------------------- + // Purpose: + // Output : byte + //----------------------------------------------------------------------------- + inline byte *GetCachedDataPointer() + { + VPROF("CAudioSourceWave::GetCachedDataPointer"); + + CAudioSourceCachedInfo *info = m_AudioCacheHandle.Get( CAudioSource::AUDIO_SOURCE_WAV, m_pSfx->IsPrecachedSound(), m_pSfx, &m_nCachedDataSize ); + if ( !info ) + { + Assert( !"CAudioSourceWave::GetCachedDataPointer info == NULL" ); + return NULL; + } + + return (byte *)info->CachedData(); + } + + int m_bits; + int m_rate; + int m_channels; + int m_format; + int m_sampleSize; + int m_loopStart; + int m_sampleCount; // can be "samples" or "bytes", depends on format + + CSfxTable *m_pSfx; + CSentence *m_pTempSentence; + + int m_dataStart; // offset of sample data + int m_dataSize; // size of sample data + + char *m_pHeader; + int m_nHeaderSize; + + CAudioSourceCachedInfoHandle_t m_AudioCacheHandle; + + int m_nCachedDataSize; + + // number of actual samples (regardless of format) + // compressed formats alter definition of m_sampleCount + // used to spare expensive calcs by decoders + int m_numDecodedSamples; + + // additional data needed by xma decoder to for looping + unsigned short m_loopBlock; // the block the loop occurs in + unsigned short m_numLeadingSamples; // number of leader samples in the loop block to discard + unsigned short m_numTrailingSamples; // number of trailing samples in the final block to discard + unsigned short unused; + + unsigned int m_bNoSentence : 1; + unsigned int m_bIsPlayOnce : 1; + unsigned int m_bIsSentenceWord : 1; + +private: + CAudioSourceWave( const CAudioSourceWave & ); // not implemented, not allowed + int m_refCount; + +#ifdef _DEBUG + // Only set in debug mode so you can see the name. + const char *m_pDebugName; +#endif +}; + + +#endif // SND_WAVE_SOURCE_H diff --git a/engine/audio/private/snd_wave_temp.cpp b/engine/audio/private/snd_wave_temp.cpp new file mode 100644 index 0000000..b1c6702 --- /dev/null +++ b/engine/audio/private/snd_wave_temp.cpp @@ -0,0 +1,149 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Create an output wave stream. Used to record audio for in-engine movies or +// mixer debugging. +// +//=====================================================================================// + +#include "audio_pch.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +extern IFileSystem *g_pFileSystem; +// FIXME: shouldn't this API be part of IFileSystem? +extern bool COM_CopyFile( const char *netpath, const char *cachepath ); + +// Create a wave file +void WaveCreateTmpFile( const char *filename, int rate, int bits, int nChannels ) +{ + char tmpfilename[MAX_PATH]; + Q_StripExtension( filename, tmpfilename, sizeof( tmpfilename ) ); + Q_DefaultExtension( tmpfilename, ".WAV", sizeof( tmpfilename ) ); + + FileHandle_t file; + file = g_pFileSystem->Open( tmpfilename, "wb" ); + if ( file == FILESYSTEM_INVALID_HANDLE ) + return; + + int chunkid = LittleLong( RIFF_ID ); + int chunksize = LittleLong( 0 ); + g_pFileSystem->Write( &chunkid, sizeof(int), file ); + g_pFileSystem->Write( &chunksize, sizeof(int), file ); + + chunkid = LittleLong( RIFF_WAVE ); + g_pFileSystem->Write( &chunkid, sizeof(int), file ); + + // create a 16-bit PCM stereo output file + PCMWAVEFORMAT fmt = { { 0 } }; + fmt.wf.wFormatTag = LittleWord( (short)WAVE_FORMAT_PCM ); + fmt.wf.nChannels = LittleWord( (short)nChannels ); + fmt.wf.nSamplesPerSec = LittleDWord( rate ); + fmt.wf.nAvgBytesPerSec = LittleDWord( rate * bits * nChannels / 8 ); + fmt.wf.nBlockAlign = LittleWord( (short)( 2 * nChannels) ); + fmt.wBitsPerSample = LittleWord( (short)bits ); + + chunkid = LittleLong( WAVE_FMT ); + chunksize = LittleLong( sizeof(fmt) ); + g_pFileSystem->Write( &chunkid, sizeof(int), file ); + g_pFileSystem->Write( &chunksize, sizeof(int), file ); + g_pFileSystem->Write( &fmt, sizeof( PCMWAVEFORMAT ), file ); + + chunkid = LittleLong( WAVE_DATA ); + chunksize = LittleLong( 0 ); + g_pFileSystem->Write( &chunkid, sizeof(int), file ); + g_pFileSystem->Write( &chunksize, sizeof(int), file ); + + g_pFileSystem->Close( file ); +} + +void WaveAppendTmpFile( const char *filename, void *pBuffer, int sampleBits, int numSamples ) +{ + char tmpfilename[MAX_PATH]; + Q_StripExtension( filename, tmpfilename, sizeof( tmpfilename ) ); + Q_DefaultExtension( tmpfilename, ".WAV", sizeof( tmpfilename ) ); + + FileHandle_t file; + file = g_pFileSystem->Open( tmpfilename, "r+b" ); + if ( file == FILESYSTEM_INVALID_HANDLE ) + return; + + g_pFileSystem->Seek( file, 0, FILESYSTEM_SEEK_TAIL ); + + if ( IsX360() && sampleBits == 16 ) + { + short *pSwapped = (short * )_alloca( numSamples * sampleBits/8 ); + for ( int i=0; i<numSamples; i++ ) + { + pSwapped[i] = LittleShort( ((short*)pBuffer)[i] ); + } + g_pFileSystem->Write( pSwapped, numSamples * sizeof( short ), file ); + } + else + { + g_pFileSystem->Write( pBuffer, numSamples * sampleBits/8, file ); + } + + g_pFileSystem->Close( file ); +} + +void WaveFixupTmpFile( const char *filename ) +{ + char tmpfilename[MAX_PATH]; + Q_StripExtension( filename, tmpfilename, sizeof( tmpfilename ) ); + Q_DefaultExtension( tmpfilename, ".WAV", sizeof( tmpfilename ) ); + + FileHandle_t file; + file = g_pFileSystem->Open( tmpfilename, "r+b" ); + if ( FILESYSTEM_INVALID_HANDLE == file ) + { + Warning( "WaveFixupTmpFile( '%s' ) failed to open file for editing\n", tmpfilename ); + return; + } + + // file size goes in RIFF chunk + int size = g_pFileSystem->Size( file ) - 2*sizeof( int ); + // offset to data chunk + int headerSize = (sizeof(int)*5 + sizeof(PCMWAVEFORMAT)); + // size of data chunk + int dataSize = size - headerSize; + + size = LittleLong( size ); + g_pFileSystem->Seek( file, sizeof( int ), FILESYSTEM_SEEK_HEAD ); + g_pFileSystem->Write( &size, sizeof( int ), file ); + + // skip the header and the 4-byte chunk tag and write the size + dataSize = LittleLong( dataSize ); + g_pFileSystem->Seek( file, headerSize+sizeof( int ), FILESYSTEM_SEEK_HEAD ); + g_pFileSystem->Write( &dataSize, sizeof( int ), file ); + + g_pFileSystem->Close( file ); +} + +CON_COMMAND( movie_fixwave, "Fixup corrupted .wav file if engine crashed during startmovie/endmovie, etc." ) +{ + if ( args.ArgC() != 2 ) + { + Msg ("Usage: movie_fixwave wavname\n"); + return; + } + + char const *wavname = args.Arg( 1 ); + if ( !g_pFileSystem->FileExists( wavname ) ) + { + Warning( "movie_fixwave: File '%s' does not exist\n", wavname ); + return; + } + + char tmpfilename[256]; + Q_StripExtension( wavname, tmpfilename, sizeof( tmpfilename ) ); + Q_strncat( tmpfilename, "_fixed", sizeof( tmpfilename ), COPY_ALL_CHARACTERS ); + Q_DefaultExtension( tmpfilename, ".wav", sizeof( tmpfilename ) ); + + // Now copy the file + Msg( "Copying '%s' to '%s'\n", wavname, tmpfilename ); + COM_CopyFile( wavname, tmpfilename ); + + Msg( "Performing fixup on '%s'\n", tmpfilename ); + WaveFixupTmpFile( tmpfilename ); +} diff --git a/engine/audio/private/snd_wave_temp.h b/engine/audio/private/snd_wave_temp.h new file mode 100644 index 0000000..6d7f74f --- /dev/null +++ b/engine/audio/private/snd_wave_temp.h @@ -0,0 +1,18 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Create an output wave stream. Used to record audio for in-engine movies or +// mixer debugging. +// +//=====================================================================================// + +#ifndef SND_WAVE_TEMP_H +#define SND_WAVE_TEMP_H +#ifdef _WIN32 +#pragma once +#endif + +extern void WaveCreateTmpFile( const char *filename, int rate, int bits, int channels ); +extern void WaveAppendTmpFile( const char *filename, void *buffer, int sampleBits, int numSamples ); +extern void WaveFixupTmpFile( const char *filename ); + +#endif // SND_WAVE_TEMP_H diff --git a/engine/audio/private/snd_win.cpp b/engine/audio/private/snd_win.cpp new file mode 100644 index 0000000..742f559 --- /dev/null +++ b/engine/audio/private/snd_win.cpp @@ -0,0 +1,185 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +//=====================================================================================// + +#include "audio_pch.h" + +#if defined( USE_SDL ) +#include "snd_dev_sdl.h" +#endif +#ifdef OSX +#include "snd_dev_openal.h" +#include "snd_dev_mac_audioqueue.h" + +ConVar snd_audioqueue( "snd_audioqueue", "1" ); + +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +bool snd_firsttime = true; + +/* + * Global variables. Must be visible to window-procedure function + * so it can unlock and free the data block after it has been played. + */ +IAudioDevice *g_AudioDevice = NULL; + +/* +================== +S_BlockSound +================== +*/ +void S_BlockSound( void ) +{ + if ( !g_AudioDevice ) + return; + + g_AudioDevice->Pause(); +} + +/* +================== +S_UnblockSound +================== +*/ +void S_UnblockSound( void ) +{ + if ( !g_AudioDevice ) + return; + + g_AudioDevice->UnPause(); +} + +/* +================== +AutoDetectInit + +Try to find a sound device to mix for. +Returns a CAudioNULLDevice if nothing is found. +================== +*/ +IAudioDevice *IAudioDevice::AutoDetectInit( bool waveOnly ) +{ + IAudioDevice *pDevice = NULL; + + if ( IsPC() ) + { +#if defined( WIN32 ) && !defined( USE_SDL ) + if ( waveOnly ) + { + pDevice = Audio_CreateWaveDevice(); + if ( !pDevice ) + goto NULLDEVICE; + } + + if ( !pDevice ) + { + if ( snd_firsttime ) + { + pDevice = Audio_CreateDirectSoundDevice(); + } + } + + // if DirectSound didn't succeed in initializing, try to initialize + // waveOut sound, unless DirectSound failed because the hardware is + // already allocated (in which case the user has already chosen not + // to have sound) + // UNDONE: JAY: This doesn't test for the hardware being in use anymore, REVISIT + if ( !pDevice ) + { + pDevice = Audio_CreateWaveDevice(); + } +#elif defined(OSX) + if ( !CommandLine()->CheckParm( "-snd_openal" ) ) + { + DevMsg( "Using AudioQueue Interface\n" ); + pDevice = Audio_CreateMacAudioQueueDevice(); + } + if ( !pDevice ) + { + DevMsg( "Using OpenAL Interface\n" ); + pDevice = Audio_CreateOpenALDevice(); // fall back to openAL if the audio queue fails + } +#elif defined( USE_SDL ) + DevMsg( "Trying SDL Audio Interface\n" ); + pDevice = Audio_CreateSDLAudioDevice(); + +#ifdef NEVER + // Jul 2012. mikesart. E-mail exchange with Ryan Gordon after figuring out that + // Audio_CreatePulseAudioDevice() wasn't working on Ubuntu 12.04 (lots of stuttering). + // + // > I installed libpulse-dev, rebuilt SDL, and now SDL is using pulse + // > audio and everything is working great. However I'm wondering if we + // > need to fall back to PulseAudio in our codebase if SDL is doing that + // > for us. I mean, is it worth me going through and debugging our Pulse + // > Audio path or should I just remove it? + // + // Remove it...it never worked well, and only remained in case there were + // concerns about relying on SDL. The SDL codepath is way easier to read, + // simpler to maintain, and handles all sorts of strange audio backends, + // including Pulse. + if ( !pDevice ) + { + DevMsg( "Trying PulseAudio Interface\n" ); + pDevice = Audio_CreatePulseAudioDevice(); // fall back to PulseAudio if SDL fails + } +#endif // NEVER + +#else +#error +#endif + } +#if defined( _X360 ) + else + { + pDevice = Audio_CreateXAudioDevice( true ); + if ( pDevice ) + { + // xaudio requires threaded mixing + S_EnableThreadedMixing( true ); + } + } +#endif + +#if defined( WIN32 ) && !defined( USE_SDL ) +NULLDEVICE: +#endif + snd_firsttime = false; + + if ( !pDevice ) + { + if ( snd_firsttime ) + DevMsg( "No sound device initialized\n" ); + + return Audio_GetNullDevice(); + } + + return pDevice; +} + +/* +============== +SNDDMA_Shutdown + +Reset the sound device for exiting +=============== +*/ +void SNDDMA_Shutdown( void ) +{ + if ( g_AudioDevice != Audio_GetNullDevice() ) + { + if ( g_AudioDevice ) + { + g_AudioDevice->Shutdown(); + delete g_AudioDevice; + } + + // the NULL device is always valid + g_AudioDevice = Audio_GetNullDevice(); + } +} + diff --git a/engine/audio/private/sound_private.h b/engine/audio/private/sound_private.h new file mode 100644 index 0000000..f054c7c --- /dev/null +++ b/engine/audio/private/sound_private.h @@ -0,0 +1,128 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "basetypes.h" +#include "snd_fixedint.h" + +#ifndef SOUND_PRIVATE_H +#define SOUND_PRIVATE_H +#pragma once + +// Forward declarations +struct portable_samplepair_t; +struct channel_t; +typedef int SoundSource; +class CAudioSource; +struct channel_t; +class CSfxTable; +class IAudioDevice; + +// ==================================================================== + +#define SAMPLE_16BIT_SHIFT 1 + +void S_Startup (void); +void S_FlushSoundData(int rate); + +CAudioSource *S_LoadSound( CSfxTable *s, channel_t *ch ); +void S_TouchSound (char *sample); +CSfxTable *S_FindName (const char *name, int *pfInCache); + +// spatializes a channel +void SND_Spatialize(channel_t *ch); +void SND_ActivateChannel( channel_t *ch ); + +// shutdown the DMA xfer. +void SNDDMA_Shutdown(void); + +// ==================================================================== +// User-setable variables +// ==================================================================== + +extern int g_paintedtime; + +extern bool snd_initialized; + +extern class Vector listener_origin; + +void S_LocalSound (char *s); + +void SND_InitScaletable (void); + +void S_AmbientOff (void); +void S_AmbientOn (void); +void S_FreeChannel(channel_t *ch); + +// resync the sample-timing adjustment clock (for scheduling a group of waves with precise timing - e.g. machine gun sounds) +extern void S_SyncClockAdjust( clocksync_index_t ); + +//============================================================================= + +// UNDONE: Move this global? +extern IAudioDevice *g_AudioDevice; + +#ifdef __cplusplus +extern "C" { +#endif // __cplusplus + +void S_TransferStereo16 (void *pOutput, const portable_samplepair_t *pfront, int lpaintedtime, int endtime); +void S_TransferPaintBuffer(void *pOutput, const portable_samplepair_t *pfront, int lpaintedtime, int endtime); +void S_MixBufferUpsample2x( int count, portable_samplepair_t *pbuffer, portable_samplepair_t *pfiltermem, int cfltmem, int filtertype ); + +extern void Mix8MonoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ); +extern void Mix8StereoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, byte *pData, int inputOffset, fixedint rateScaleFix, int outCount ); +extern void Mix16MonoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ); +extern void Mix16StereoWavtype( channel_t *pChannel, portable_samplepair_t *pOutput, int *volume, short *pData, int inputOffset, fixedint rateScaleFix, int outCount ); + +extern void SND_MoveMouth8(channel_t *pChannel, CAudioSource *pSource, int count); +extern void SND_CloseMouth(channel_t *pChannel); +extern void SND_InitMouth( channel_t *pChannel ); +extern void SND_UpdateMouth( channel_t *pChannel ); +extern void SND_ClearMouth( channel_t *pChannel ); +extern bool SND_IsMouth( channel_t *pChannel ); +extern bool SND_ShouldPause( channel_t *pChannel ); +extern bool SND_IsRecording(); + +void MIX_PaintChannels( int endtime, bool bIsUnderwater ); +// Play a big of zeroed out sound +void MIX_PaintNullChannels( int endtime ); + +bool AllocDsps( bool bLoadPresetFile ); +void FreeDsps( bool bReleaseTemplateMemory ); +void ForceCleanDspPresets( void ); +void CheckNewDspPresets( void ); + +void DSP_Process( int idsp, portable_samplepair_t *pbfront, portable_samplepair_t *pbrear, portable_samplepair_t *pbcenter, int sampleCount ); +void DSP_ClearState(); + +extern int idsp_room; +extern int idsp_water; +extern int idsp_player; +extern int idsp_facingaway; +extern int idsp_speaker; +extern int idsp_spatial; + +extern float g_DuckScale; + +// Legacy DSP Routines + +void SX_Init (void); +void SX_Free (void); +void SX_ReloadRoomFX(); +void SX_RoomFX(int endtime, int fFilter, int fTimefx); + +// DSP Routines + +void DSP_InitAll(bool bLoadPresetFile); +void DSP_FreeAll(void); + +#ifdef __cplusplus +} +#endif // __cplusplus + +#endif // SOUND_PRIVATE_H diff --git a/engine/audio/private/voice.cpp b/engine/audio/private/voice.cpp new file mode 100644 index 0000000..ce506d1 --- /dev/null +++ b/engine/audio/private/voice.cpp @@ -0,0 +1,1566 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +#include "audio_pch.h" +#include "circularbuffer.h" +#include "voice.h" +#include "voice_wavefile.h" +#include "r_efx.h" +#include "cdll_int.h" +#include "voice_gain.h" +#include "voice_mixer_controls.h" + +#include "ivoicerecord.h" +#include "ivoicecodec.h" +#include "filesystem.h" +#include "../../filesystem_engine.h" +#include "tier1/utlbuffer.h" +#if defined( _X360 ) +#include "xauddefs.h" +#endif + +#include "steam/steam_api.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static CSteamAPIContext g_SteamAPIContext; +static CSteamAPIContext *steamapicontext = NULL; + +void Voice_EndChannel( int iChannel ); +void VoiceTweak_EndVoiceTweakMode(); +void EngineTool_OverrideSampleRate( int& rate ); + +// A fallback codec that should be the most likely to work for local/offline use +#define VOICE_FALLBACK_CODEC "vaudio_celt" + +// Special entity index used for tweak mode. +#define TWEAKMODE_ENTITYINDEX -500 + +// Special channel index passed to Voice_AddIncomingData when in tweak mode. +#define TWEAKMODE_CHANNELINDEX -100 + + +// How long does the sign stay above someone's head when they talk? +#define SPARK_TIME 0.2 + +// How long a voice channel has to be inactive before we free it. +#define DIE_COUNTDOWN 0.5 + +#define VOICE_RECEIVE_BUFFER_SIZE (VOICE_OUTPUT_SAMPLE_RATE_MAX * BYTES_PER_SAMPLE) + +#define LOCALPLAYERTALKING_TIMEOUT 0.2f // How long it takes for the client to decide the server isn't sending acks + // of voice data back. + +// If this is defined, then the data is converted to 8-bit and sent otherwise uncompressed. +// #define VOICE_SEND_RAW_TEST + +// The format we sample voice in. +WAVEFORMATEX g_VoiceSampleFormat = +{ + WAVE_FORMAT_PCM, // wFormatTag + 1, // nChannels + // These two can be dynamically changed by voice_init + VOICE_OUTPUT_SAMPLE_RATE_LOW, // nSamplesPerSec + VOICE_OUTPUT_SAMPLE_RATE_LOW*2, // nAvgBytesPerSec + 2, // nBlockAlign + 16, // wBitsPerSample + sizeof(WAVEFORMATEX) // cbSize +}; + +static bool Voice_SetSampleRate( DWORD rate ) +{ + if ( g_VoiceSampleFormat.nSamplesPerSec != rate || + g_VoiceSampleFormat.nAvgBytesPerSec != rate * 2 ) + { + g_VoiceSampleFormat.nSamplesPerSec = rate; + g_VoiceSampleFormat.nAvgBytesPerSec = rate * 2; + return true; + } + + return false; +} + +int Voice_SamplesPerSec() +{ + int rate = g_VoiceSampleFormat.nSamplesPerSec; + EngineTool_OverrideSampleRate( rate ); + return rate; +} + +int Voice_AvgBytesPerSec() +{ + int rate = g_VoiceSampleFormat.nSamplesPerSec; + EngineTool_OverrideSampleRate( rate ); + return ( rate * g_VoiceSampleFormat.wBitsPerSample ) >> 3; +} + +ConVar voice_avggain( "voice_avggain", "0.5" ); +ConVar voice_maxgain( "voice_maxgain", "10" ); +ConVar voice_scale( "voice_scale", "1", FCVAR_ARCHIVE ); + +ConVar voice_loopback( "voice_loopback", "0", FCVAR_USERINFO ); +ConVar voice_fadeouttime( "voice_fadeouttime", "0.1" ); // It fades to no sound at the tail end of your voice data when you release the key. + +// Debugging cvars. +ConVar voice_profile( "voice_profile", "0" ); +ConVar voice_showchannels( "voice_showchannels", "0" ); // 1 = list channels + // 2 = show timing info, etc +ConVar voice_showincoming( "voice_showincoming", "0" ); // show incoming voice data + +ConVar voice_enable( "voice_enable", "1", FCVAR_ARCHIVE ); // Globally enable or disable voice. +#ifdef VOICE_VOX_ENABLE +ConVar voice_threshold( "voice_threshold", "2000", FCVAR_ARCHIVE ); +#endif // VOICE_VOX_ENABLE + +// Have it force your mixer control settings so waveIn comes from the microphone. +// CD rippers change your waveIn to come from the CD drive +ConVar voice_forcemicrecord( "voice_forcemicrecord", "1", FCVAR_ARCHIVE ); + +// This should not be lower than the maximum difference between clients' frame durations (due to cmdrate/updaterate), +// plus some jitter allowance. +ConVar voice_buffer_ms( "voice_buffer_ms", "100", FCVAR_INTERNAL_USE, + "How many milliseconds of voice to buffer to avoid dropouts due to jitter and frame time differences." ); + +int g_nVoiceFadeSamples = 1; // Calculated each frame from the cvar. +float g_VoiceFadeMul = 1; // 1 / (g_nVoiceFadeSamples - 1). + +// While in tweak mode, you can't hear anything anyone else is saying, and your own voice data +// goes directly to the speakers. +bool g_bInTweakMode = false; +int g_VoiceTweakSpeakingVolume = 0; + +bool g_bVoiceAtLeastPartiallyInitted = false; + +// The codec and sample rate passed to Voice_Init. "" and -1 if voice is not initialized +char g_szVoiceCodec[_MAX_PATH] = { 0 }; +int g_nVoiceRequestedSampleRate = -1; + +const char *Voice_ConfiguredCodec() { return g_szVoiceCodec; } +int Voice_ConfiguredSampleRate() { return g_nVoiceRequestedSampleRate; } + +// Timing info for each frame. +static double g_CompressTime = 0; +static double g_DecompressTime = 0; +static double g_GainTime = 0; +static double g_UpsampleTime = 0; + +class CVoiceTimer +{ +public: + inline void Start() + { + if( voice_profile.GetInt() ) + { + m_StartTime = Plat_FloatTime(); + } + } + + inline void End(double *out) + { + if( voice_profile.GetInt() ) + { + *out += Plat_FloatTime() - m_StartTime; + } + } + + double m_StartTime; +}; + + +static bool g_bLocalPlayerTalkingAck = false; +static float g_LocalPlayerTalkingTimeout = 0; + + +CSysModule *g_hVoiceCodecDLL = 0; + +// Voice recorder. Can be waveIn, DSound, or whatever. +static IVoiceRecord *g_pVoiceRecord = NULL; +static IVoiceCodec *g_pEncodeCodec = NULL; + +static bool g_bVoiceRecording = false; // Are we recording at the moment? +static bool g_bVoiceRecordStopping = false; // Are we waiting to stop? +bool g_bUsingSteamVoice = false; + +#ifdef WIN32 +extern IVoiceRecord* CreateVoiceRecord_DSound(int nSamplesPerSec); +#elif defined( OSX ) +extern IVoiceRecord* CreateVoiceRecord_AudioQueue(int sampleRate); +#endif + +#ifdef POSIX +extern IVoiceRecord* CreateVoiceRecord_OpenAL(int sampleRate); +#endif + + +static bool VoiceRecord_Start() +{ + if ( g_bUsingSteamVoice ) + { + if ( steamapicontext && steamapicontext->SteamUser() ) + { + steamapicontext->SteamUser()->StartVoiceRecording(); + return true; + } + } + else if ( g_pVoiceRecord ) + { + return g_pVoiceRecord->RecordStart(); + } + return false; +} + +static void VoiceRecord_Stop() +{ + if ( g_bUsingSteamVoice ) + { + if ( steamapicontext && steamapicontext->SteamUser() ) + { + steamapicontext->SteamUser()->StopVoiceRecording(); + } + } + else if ( g_pVoiceRecord ) + { + return g_pVoiceRecord->RecordStop(); + } +} + +// +// Used for storing incoming voice data from an entity. +// +class CVoiceChannel +{ +public: + CVoiceChannel(); + + // Called when someone speaks and a new voice channel is setup to hold the data. + void Init(int nEntity); + +public: + int m_iEntity; // Number of the entity speaking on this channel (index into cl_entities). + // This is -1 when the channel is unused. + + + CSizedCircularBuffer + <VOICE_RECEIVE_BUFFER_SIZE> m_Buffer; // Circular buffer containing the voice data. + + // Used for upsampling.. + double m_LastFraction; // Fraction between m_LastSample and the next sample. + short m_LastSample; + + bool m_bStarved; // Set to true when the channel runs out of data for the mixer. + // The channel is killed at that point. + + float m_TimePad; // Set to TIME_PADDING when the first voice packet comes in from a sender. + // We add time padding (for frametime differences) + // by waiting a certain amount of time before starting to output the sound. + + IVoiceCodec *m_pVoiceCodec; // Each channel gets is own IVoiceCodec instance so the codec can maintain state. + + CAutoGain m_AutoGain; // Gain we're applying to this channel + + CVoiceChannel *m_pNext; + + bool m_bProximity; + int m_nViewEntityIndex; + int m_nSoundGuid; +}; + + +CVoiceChannel::CVoiceChannel() +{ + m_iEntity = -1; + m_pVoiceCodec = NULL; + m_nViewEntityIndex = -1; + m_nSoundGuid = -1; +} + +void CVoiceChannel::Init(int nEntity) +{ + m_iEntity = nEntity; + m_bStarved = false; + m_Buffer.Flush(); + m_TimePad = Clamp( voice_buffer_ms.GetFloat(), 1.f, 5000.f ) / 1000.f; + m_LastSample = 0; + m_LastFraction = 0.999; + + m_AutoGain.Reset( 128, voice_maxgain.GetFloat(), voice_avggain.GetFloat(), voice_scale.GetFloat() ); +} + + + +// Incoming voice channels. +CVoiceChannel g_VoiceChannels[VOICE_NUM_CHANNELS]; + + +// These are used for recording the wave data into files for debugging. +#define MAX_WAVEFILEDATA_LEN 1024*1024 +char *g_pUncompressedFileData = NULL; +int g_nUncompressedDataBytes = 0; +const char *g_pUncompressedDataFilename = NULL; + +char *g_pDecompressedFileData = NULL; +int g_nDecompressedDataBytes = 0; +const char *g_pDecompressedDataFilename = NULL; + +char *g_pMicInputFileData = NULL; +int g_nMicInputFileBytes = 0; +int g_CurMicInputFileByte = 0; +double g_MicStartTime; + +static ConVar voice_writevoices( "voice_writevoices", "0", 0, "Saves each speaker's voice data into separate .wav files\n" ); +class CVoiceWriterData +{ +public: + CVoiceWriterData() : + m_pChannel( NULL ), + m_nCount( 0 ), + m_Buffer() + { + } + + // Copy ctor is needed to insert into CVoiceWriter's CUtlRBTree. + CVoiceWriterData(const CVoiceWriterData& other) : + m_pChannel( other.m_pChannel ), + m_nCount( other.m_nCount ), + m_Buffer( ) + { + m_Buffer.CopyBuffer( other.m_Buffer ); + } + + static bool Less( const CVoiceWriterData &lhs, const CVoiceWriterData &rhs ) + { + return lhs.m_pChannel < rhs.m_pChannel; + } + + CVoiceChannel *m_pChannel; + int m_nCount; + CUtlBuffer m_Buffer; + +private: + CVoiceWriterData& operator=(const CVoiceWriterData&); +}; + +class CVoiceWriter +{ +public: + CVoiceWriter() : + m_VoiceWriter( 0, 0, CVoiceWriterData::Less ) + { + } + + void Flush() + { + for ( int i = m_VoiceWriter.FirstInorder(); i != m_VoiceWriter.InvalidIndex(); i = m_VoiceWriter.NextInorder( i ) ) + { + CVoiceWriterData *data = &m_VoiceWriter[ i ]; + + if ( data->m_Buffer.TellPut() <= 0 ) + continue; + data->m_Buffer.Purge(); + } + } + + void Finish() + { + if ( !g_pSoundServices->IsConnected() ) + { + Flush(); + return; + } + + for ( int i = m_VoiceWriter.FirstInorder(); i != m_VoiceWriter.InvalidIndex(); i = m_VoiceWriter.NextInorder( i ) ) + { + CVoiceWriterData *data = &m_VoiceWriter[ i ]; + + if ( data->m_Buffer.TellPut() <= 0 ) + continue; + + int index = data->m_pChannel - g_VoiceChannels; + Assert( index >= 0 && index < (int)ARRAYSIZE( g_VoiceChannels ) ); + + char path[ MAX_PATH ]; + Q_snprintf( path, sizeof( path ), "%s/voice", g_pSoundServices->GetGameDir() ); + g_pFileSystem->CreateDirHierarchy( path ); + + char fn[ MAX_PATH ]; + Q_snprintf( fn, sizeof( fn ), "%s/pl%02d_slot%d-time%d.wav", path, index, data->m_nCount, (int)g_pSoundServices->GetClientTime() ); + + WriteWaveFile( fn, (const char *)data->m_Buffer.Base(), data->m_Buffer.TellPut(), g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() ); + + Msg( "Writing file %s\n", fn ); + + ++data->m_nCount; + data->m_Buffer.Purge(); + } + } + + + void AddDecompressedData( CVoiceChannel *ch, const byte *data, size_t datalen ) + { + if ( !voice_writevoices.GetBool() ) + return; + + CVoiceWriterData search; + search.m_pChannel = ch; + int idx = m_VoiceWriter.Find( search ); + if ( idx == m_VoiceWriter.InvalidIndex() ) + { + idx = m_VoiceWriter.Insert( search ); + } + + CVoiceWriterData *slot = &m_VoiceWriter[ idx ]; + slot->m_Buffer.Put( data, datalen ); + } +private: + + CUtlRBTree< CVoiceWriterData > m_VoiceWriter; +}; + +static CVoiceWriter g_VoiceWriter; + +inline void ApplyFadeToSamples(short *pSamples, int nSamples, int fadeOffset, float fadeMul) +{ + for(int i=0; i < nSamples; i++) + { + float percent = (i+fadeOffset) * fadeMul; + pSamples[i] = (short)(pSamples[i] * (1 - percent)); + } +} + + +bool Voice_Enabled( void ) +{ + return voice_enable.GetBool(); +} + + +int Voice_GetOutputData( + const int iChannel, //! The voice channel it wants samples from. + char *copyBufBytes, //! The buffer to copy the samples into. + const int copyBufSize, //! Maximum size of copyBuf. + const int samplePosition, //! Which sample to start at. + const int sampleCount //! How many samples to get. +) +{ + CVoiceChannel *pChannel = &g_VoiceChannels[iChannel]; + short *pCopyBuf = (short*)copyBufBytes; + + + int maxOutSamples = copyBufSize / BYTES_PER_SAMPLE; + + // Find out how much we want and get it from the received data channel. + CCircularBuffer *pBuffer = &pChannel->m_Buffer; + int nBytesToRead = pBuffer->GetReadAvailable(); + nBytesToRead = min(min(nBytesToRead, (int)maxOutSamples), sampleCount * BYTES_PER_SAMPLE); + int nSamplesGotten = pBuffer->Read(pCopyBuf, nBytesToRead) / BYTES_PER_SAMPLE; + + // Are we at the end of the buffer's data? If so, fade data to silence so it doesn't clip. + int readSamplesAvail = pBuffer->GetReadAvailable() / BYTES_PER_SAMPLE; + if(readSamplesAvail < g_nVoiceFadeSamples) + { + int bufferFadeOffset = max((readSamplesAvail + nSamplesGotten) - g_nVoiceFadeSamples, 0); + int globalFadeOffset = max(g_nVoiceFadeSamples - (readSamplesAvail + nSamplesGotten), 0); + + ApplyFadeToSamples( + &pCopyBuf[bufferFadeOffset], + nSamplesGotten - bufferFadeOffset, + globalFadeOffset, + g_VoiceFadeMul); + } + + // If there weren't enough samples in the received data channel, + // pad it with a copy of the most recent data, and if there + // isn't any, then use zeros. + if ( nSamplesGotten < sampleCount ) + { + int wantedSampleCount = min( sampleCount, maxOutSamples ); + int nAdditionalNeeded = (wantedSampleCount - nSamplesGotten); + if ( nSamplesGotten > 0 ) + { + short *dest = (short *)&pCopyBuf[ nSamplesGotten ]; + int nSamplesToDuplicate = min( nSamplesGotten, nAdditionalNeeded ); + const short *src = (short *)&pCopyBuf[ nSamplesGotten - nSamplesToDuplicate ]; + + Q_memcpy( dest, src, nSamplesToDuplicate * BYTES_PER_SAMPLE ); + + //Msg( "duplicating %d samples\n", nSamplesToDuplicate ); + + nAdditionalNeeded -= nSamplesToDuplicate; + if ( nAdditionalNeeded > 0 ) + { + dest = (short *)&pCopyBuf[ nSamplesGotten + nSamplesToDuplicate ]; + Q_memset(dest, 0, nAdditionalNeeded * BYTES_PER_SAMPLE); + + // Msg( "zeroing %d samples\n", nAdditionalNeeded ); + + Assert( ( nAdditionalNeeded + nSamplesGotten + nSamplesToDuplicate ) == wantedSampleCount ); + } + } + else + { + Q_memset( &pCopyBuf[ nSamplesGotten ], 0, nAdditionalNeeded * BYTES_PER_SAMPLE ); + } + nSamplesGotten = wantedSampleCount; + } + + // If the buffer is out of data, mark this channel to go away. + if(pBuffer->GetReadAvailable() == 0) + { + pChannel->m_bStarved = true; + } + + if(voice_showchannels.GetInt() >= 2) + { + Msg("Voice - mixed %d samples from channel %d\n", nSamplesGotten, iChannel); + } + + VoiceSE_MoveMouth(pChannel->m_iEntity, (short*)copyBufBytes, nSamplesGotten); + return nSamplesGotten; +} + + +void Voice_OnAudioSourceShutdown( int iChannel ) +{ + Voice_EndChannel( iChannel ); +} + + +// ------------------------------------------------------------------------ // +// Internal stuff. +// ------------------------------------------------------------------------ // + +CVoiceChannel* GetVoiceChannel(int iChannel, bool bAssert=true) +{ + if(iChannel < 0 || iChannel >= VOICE_NUM_CHANNELS) + { + if(bAssert) + { + Assert(false); + } + return NULL; + } + else + { + return &g_VoiceChannels[iChannel]; + } +} + +// Helper for doing a default-init with some codec if we weren't passed specific parameters +bool Voice_InitWithDefault( const char *pCodecName ) +{ + if ( !pCodecName || !*pCodecName ) + { + return false; + } + + int nRate = Voice_GetDefaultSampleRate( pCodecName ); + if ( nRate < 0 ) + { + Msg( "Voice_InitWithDefault: Unable to determine defaults for codec \"%s\"\n", pCodecName ); + return false; + } + + return Voice_Init( pCodecName, Voice_GetDefaultSampleRate( pCodecName ) ); +} + +bool Voice_Init( const char *pCodecName, int nSampleRate ) +{ + if ( voice_enable.GetInt() == 0 ) + { + return false; + } + + if ( !pCodecName || !pCodecName[0] ) + { + return false; + } + + bool bSpeex = Q_stricmp( pCodecName, "vaudio_speex" ) == 0; + bool bCelt = Q_stricmp( pCodecName, "vaudio_celt" ) == 0; + bool bSteam = Q_stricmp( pCodecName, "steam" ) == 0; + // Miles has not been in use for voice in a long long time. Not worth the surface to support ancient demos that may + // use it (and probably do not work for other reasons) + // "vaudio_miles" + + if ( !( bSpeex || bCelt || bSteam ) ) + { + Msg( "Voice_Init Failed: invalid voice codec %s.\n", pCodecName ); + return false; + } + + Voice_Deinit(); + + g_bVoiceAtLeastPartiallyInitted = true; + V_strncpy( g_szVoiceCodec, pCodecName, sizeof(g_szVoiceCodec) ); + g_nVoiceRequestedSampleRate = nSampleRate; + + g_bUsingSteamVoice = bSteam; + + if ( !steamapicontext ) + { + steamapicontext = &g_SteamAPIContext; + steamapicontext->Init(); + } + + if ( g_bUsingSteamVoice ) + { + if ( !steamapicontext->SteamFriends() || !steamapicontext->SteamUser() ) + { + Msg( "Voice_Init: Requested Steam voice, but cannot access API. Voice will not function\n" ); + return false; + } + } + + // For steam, nSampleRate 0 means "use optimal steam sample rate". + if ( bSteam && nSampleRate == 0 ) + { + Msg( "Voice_Init: Using Steam voice optimal sample rate %d\n", + steamapicontext->SteamUser()->GetVoiceOptimalSampleRate() ); + // Steam's sample rate may change and not be supported by our rather unflexible sound engine. However, steam + // will resample as necessary in DecompressVoice, so we can pretend we're outputting at native rates. + // + // Behind the scenes, we'll request steam give us the encoded stream at its "optimal" rate, then we'll try to + // decompress the output at this rate, making it transparent to us that the encoded stream is not at our output + // rate. + Voice_SetSampleRate( SOUND_DMA_SPEED ); + } + else + { + Voice_SetSampleRate( nSampleRate ); + } + + if(!VoiceSE_Init()) + return false; + + // Get the voice input device. +#ifdef OSX + g_pVoiceRecord = CreateVoiceRecord_AudioQueue( Voice_SamplesPerSec() ); + if ( !g_pVoiceRecord ) + { + // Fall back to OpenAL + g_pVoiceRecord = CreateVoiceRecord_OpenAL( Voice_SamplesPerSec() ); + } +#elif defined( WIN32 ) + g_pVoiceRecord = CreateVoiceRecord_DSound( Voice_SamplesPerSec() ); +#else + g_pVoiceRecord = CreateVoiceRecord_OpenAL( Voice_SamplesPerSec() ); +#endif + + if( !g_pVoiceRecord ) + { + Msg( "Unable to initialize sound capture. You won't be able to speak to other players." ); + } + + // Init codec DLL for non-steam + if ( !bSteam ) + { + // CELT's qualities are 0-3, we historically just passed 4 to the other two even though they don't really map to the + // same thing. + // + // Changing the quality level we use here will require either extending SVC_VoiceInit to pass down which quality is + // in use or using a different codec name (vaudio_celtHD!) for backwards compatibility + int quality = bCelt ? 3 : 4; + + // Get the codec. + CreateInterfaceFn createCodecFn = NULL; + g_hVoiceCodecDLL = FileSystem_LoadModule(pCodecName); + + if ( !g_hVoiceCodecDLL || (createCodecFn = Sys_GetFactory(g_hVoiceCodecDLL)) == NULL || + (g_pEncodeCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !g_pEncodeCodec->Init( quality ) ) + { + Msg("Unable to load voice codec '%s'. Voice disabled. (module %i, iface %i, codec %i)\n", + pCodecName, !!g_hVoiceCodecDLL, !!createCodecFn, !!g_pEncodeCodec); + Voice_Deinit(); + return false; + } + + for (int i=0; i < VOICE_NUM_CHANNELS; i++) + { + CVoiceChannel *pChannel = &g_VoiceChannels[i]; + + if ((pChannel->m_pVoiceCodec = (IVoiceCodec*)createCodecFn(pCodecName, NULL)) == NULL || !pChannel->m_pVoiceCodec->Init( quality )) + { + Voice_Deinit(); + return false; + } + } + } + + // XXX(JohnS): These don't do much in Steam codec mode, but code below uses their presence to mean 'voice fully + // initialized' and other things assume they will succeed. + InitMixerControls(); + + // Steam mode uses steam for raw input so this isn't meaningful and could have side-effects + if( voice_forcemicrecord.GetInt() && !bSteam ) + { + if( g_pMixerControls ) + g_pMixerControls->SelectMicrophoneForWaveInput(); + } + + return true; +} + + +void Voice_EndChannel(int iChannel) +{ + Assert(iChannel >= 0 && iChannel < VOICE_NUM_CHANNELS); + + CVoiceChannel *pChannel = &g_VoiceChannels[iChannel]; + + if ( pChannel->m_iEntity != -1 ) + { + int iEnt = pChannel->m_iEntity; + pChannel->m_iEntity = -1; + + if ( pChannel->m_bProximity == true ) + { + VoiceSE_EndChannel( iChannel, iEnt ); + } + else + { + VoiceSE_EndChannel( iChannel, pChannel->m_nViewEntityIndex ); + } + + g_pSoundServices->OnChangeVoiceStatus( iEnt, false ); + VoiceSE_CloseMouth( iEnt ); + + pChannel->m_nViewEntityIndex = -1; + pChannel->m_nSoundGuid = -1; + + // If the tweak mode channel is ending + if ( iChannel == 0 && + g_bInTweakMode ) + { + VoiceTweak_EndVoiceTweakMode(); + } + } +} + + +void Voice_EndAllChannels() +{ + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + { + Voice_EndChannel(i); + } +} + +bool EngineTool_SuppressDeInit(); + +void Voice_Deinit() +{ + // This call tends to be expensive and when voice is not enabled it will continually + // call in here, so avoid the work if possible. + if( !g_bVoiceAtLeastPartiallyInitted ) + return; + + if ( EngineTool_SuppressDeInit() ) + return; + + Voice_EndAllChannels(); + + Voice_RecordStop(); + + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + { + CVoiceChannel *pChannel = &g_VoiceChannels[i]; + + if ( pChannel->m_pVoiceCodec ) + { + pChannel->m_pVoiceCodec->Release(); + pChannel->m_pVoiceCodec = NULL; + } + } + + if( g_pEncodeCodec ) + { + g_pEncodeCodec->Release(); + g_pEncodeCodec = NULL; + } + + if(g_hVoiceCodecDLL) + { + FileSystem_UnloadModule(g_hVoiceCodecDLL); + g_hVoiceCodecDLL = NULL; + } + + if(g_pVoiceRecord) + { + g_pVoiceRecord->Release(); + g_pVoiceRecord = NULL; + } + + VoiceSE_Term(); + + g_bVoiceAtLeastPartiallyInitted = false; + g_szVoiceCodec[0] = '\0'; + g_nVoiceRequestedSampleRate = -1; + g_bUsingSteamVoice = false; +} + +bool Voice_GetLoopback() +{ + return !!voice_loopback.GetInt(); +} + + +void Voice_LocalPlayerTalkingAck() +{ + if(!g_bLocalPlayerTalkingAck) + { + // Tell the client DLL when this changes. + g_pSoundServices->OnChangeVoiceStatus(-2, TRUE); + } + + g_bLocalPlayerTalkingAck = true; + g_LocalPlayerTalkingTimeout = 0; +} + + +void Voice_UpdateVoiceTweakMode() +{ + if(!g_bInTweakMode || !g_pVoiceRecord) + return; + + CVoiceChannel *pTweakChannel = GetVoiceChannel( 0 ); + + if ( pTweakChannel->m_nSoundGuid != -1 && + !S_IsSoundStillPlaying( pTweakChannel->m_nSoundGuid ) ) + { + VoiceTweak_EndVoiceTweakMode(); + return; + } + + char uchVoiceData[4096]; + bool bFinal = false; + int nDataLength = Voice_GetCompressedData(uchVoiceData, sizeof(uchVoiceData), bFinal); + + Voice_AddIncomingData(TWEAKMODE_CHANNELINDEX, uchVoiceData, nDataLength, 0); +} + + +void Voice_Idle(float frametime) +{ + if( voice_enable.GetInt() == 0 ) + { + Voice_Deinit(); + return; + } + + if( g_bLocalPlayerTalkingAck ) + { + g_LocalPlayerTalkingTimeout += frametime; + if(g_LocalPlayerTalkingTimeout > LOCALPLAYERTALKING_TIMEOUT) + { + g_bLocalPlayerTalkingAck = false; + + // Tell the client DLL. + g_pSoundServices->OnChangeVoiceStatus(-2, FALSE); + } + } + + // Precalculate these to speedup the voice fadeout. + g_nVoiceFadeSamples = max((int)(voice_fadeouttime.GetFloat() * g_VoiceSampleFormat.nSamplesPerSec ), 2); + g_VoiceFadeMul = 1.0f / (g_nVoiceFadeSamples - 1); + + if(g_pVoiceRecord) + g_pVoiceRecord->Idle(); + + // If we're in voice tweak mode, feed our own data back to us. + Voice_UpdateVoiceTweakMode(); + + // Age the channels. + int nActive = 0; + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + { + CVoiceChannel *pChannel = &g_VoiceChannels[i]; + + if(pChannel->m_iEntity != -1) + { + if(pChannel->m_bStarved) + { + // Kill the channel. It's done playing. + Voice_EndChannel(i); + pChannel->m_nSoundGuid = -1; + } + else + { + float oldpad = pChannel->m_TimePad; + pChannel->m_TimePad -= frametime; + if(oldpad > 0 && pChannel->m_TimePad <= 0) + { + // Start its audio. + pChannel->m_nViewEntityIndex = g_pSoundServices->GetViewEntity(); + pChannel->m_nSoundGuid = VoiceSE_StartChannel( i, pChannel->m_iEntity, pChannel->m_bProximity, pChannel->m_nViewEntityIndex ); + g_pSoundServices->OnChangeVoiceStatus(pChannel->m_iEntity, TRUE); + + VoiceSE_InitMouth(pChannel->m_iEntity); + } + + ++nActive; + } + } + } + + if(nActive == 0) + VoiceSE_EndOverdrive(); + + VoiceSE_Idle(frametime); + + // voice_showchannels. + if( voice_showchannels.GetInt() >= 1 ) + { + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + { + CVoiceChannel *pChannel = &g_VoiceChannels[i]; + + if(pChannel->m_iEntity == -1) + continue; + + Msg("Voice - chan %d, ent %d, bufsize: %d\n", i, pChannel->m_iEntity, pChannel->m_Buffer.GetReadAvailable()); + } + } + + // Show profiling data? + if( voice_profile.GetInt() ) + { + Msg("Voice - compress: %7.2fu, decompress: %7.2fu, gain: %7.2fu, upsample: %7.2fu, total: %7.2fu\n", + g_CompressTime*1000000.0, + g_DecompressTime*1000000.0, + g_GainTime*1000000.0, + g_UpsampleTime*1000000.0, + (g_CompressTime+g_DecompressTime+g_GainTime+g_UpsampleTime)*1000000.0 + ); + + g_CompressTime = g_DecompressTime = g_GainTime = g_UpsampleTime = 0; + } +} + + +bool Voice_IsRecording() +{ + return g_bVoiceRecording && !g_bInTweakMode; +} + + +bool Voice_RecordStart( + const char *pUncompressedFile, + const char *pDecompressedFile, + const char *pMicInputFile) +{ + if( !g_pEncodeCodec && !g_bUsingSteamVoice ) + return false; + + g_VoiceWriter.Flush(); + + Voice_RecordStop(); + + if ( !g_bUsingSteamVoice ) + { + g_pEncodeCodec->ResetState(); + } + + if(pMicInputFile) + { + int a, b, c; + ReadWaveFile(pMicInputFile, g_pMicInputFileData, g_nMicInputFileBytes, a, b, c); + g_CurMicInputFileByte = 0; + g_MicStartTime = Plat_FloatTime(); + } + + if(pUncompressedFile) + { + g_pUncompressedFileData = new char[MAX_WAVEFILEDATA_LEN]; + g_nUncompressedDataBytes = 0; + g_pUncompressedDataFilename = pUncompressedFile; + } + + if(pDecompressedFile) + { + g_pDecompressedFileData = new char[MAX_WAVEFILEDATA_LEN]; + g_nDecompressedDataBytes = 0; + g_pDecompressedDataFilename = pDecompressedFile; + } + + g_bVoiceRecording = false; + if ( g_pVoiceRecord ) + { + g_bVoiceRecording = VoiceRecord_Start(); + if ( g_bVoiceRecording ) + { + if ( steamapicontext && steamapicontext->SteamFriends() && steamapicontext->SteamUser() ) + { + // Tell Friends' Voice chat that the local user has started speaking + steamapicontext->SteamFriends()->SetInGameVoiceSpeaking( steamapicontext->SteamUser()->GetSteamID(), true ); + } + + g_pSoundServices->OnChangeVoiceStatus( -1, true ); // Tell the client DLL. + } + } + + return g_bVoiceRecording; +} + + +void Voice_UserDesiresStop() +{ + if ( g_bVoiceRecordStopping ) + return; + + g_bVoiceRecordStopping = true; + g_pSoundServices->OnChangeVoiceStatus( -1, false ); // Tell the client DLL. + + // If we're using Steam voice, we'll keep recording until Steam tells us we + // received all the data. + if ( g_bUsingSteamVoice ) + { + steamapicontext->SteamUser()->StopVoiceRecording(); + } + else + { + VoiceRecord_Stop(); + } +} + + +bool Voice_RecordStop() +{ + // Write the files out for debugging. + if(g_pMicInputFileData) + { + delete [] g_pMicInputFileData; + g_pMicInputFileData = NULL; + } + + if(g_pUncompressedFileData) + { + WriteWaveFile(g_pUncompressedDataFilename, g_pUncompressedFileData, g_nUncompressedDataBytes, g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() ); + delete [] g_pUncompressedFileData; + g_pUncompressedFileData = NULL; + } + + if(g_pDecompressedFileData) + { + WriteWaveFile(g_pDecompressedDataFilename, g_pDecompressedFileData, g_nDecompressedDataBytes, g_VoiceSampleFormat.wBitsPerSample, g_VoiceSampleFormat.nChannels, Voice_SamplesPerSec() ); + delete [] g_pDecompressedFileData; + g_pDecompressedFileData = NULL; + } + + g_VoiceWriter.Finish(); + + VoiceRecord_Stop(); + + if ( g_bVoiceRecording ) + { + if ( steamapicontext->SteamFriends() && steamapicontext->SteamUser() ) + { + // Tell Friends' Voice chat that the local user has stopped speaking + steamapicontext->SteamFriends()->SetInGameVoiceSpeaking( steamapicontext->SteamUser()->GetSteamID(), false ); + } + } + + g_bVoiceRecording = false; + g_bVoiceRecordStopping = false; + return(true); +} + + +int Voice_GetCompressedData(char *pchDest, int nCount, bool bFinal) +{ + // Check g_bVoiceRecordStopping in case g_bUsingSteamVoice changes on us + // while waiting for the end of voice data. + if ( g_bUsingSteamVoice || g_bVoiceRecordStopping ) + { + uint32 cbCompressedWritten = 0; + uint32 cbUncompressedWritten = 0; + uint32 cbCompressed = 0; + uint32 cbUncompressed = 0; + // We're going to always request steam give us the encoded stream at the optimal rate, unless our final output + // rate is lower than it. We'll pass our output rate when we actually extract the data, which Steam will + // happily upsample from its optimal rate for us. + int nEncodeRate = min( (int)steamapicontext->SteamUser()->GetVoiceOptimalSampleRate(), Voice_SamplesPerSec() ); + EVoiceResult result = steamapicontext->SteamUser()->GetAvailableVoice( &cbCompressed, &cbUncompressed, nEncodeRate ); + if ( result == k_EVoiceResultOK ) + { + result = steamapicontext->SteamUser()->GetVoice( true, pchDest, nCount, &cbCompressedWritten, + g_pUncompressedFileData != NULL, g_pUncompressedFileData, + MAX_WAVEFILEDATA_LEN - g_nUncompressedDataBytes, + &cbUncompressedWritten, nEncodeRate ); + + if ( g_pUncompressedFileData ) + { + g_nUncompressedDataBytes += cbUncompressedWritten; + } + g_pSoundServices->OnChangeVoiceStatus( -3, true ); + } + else + { + if ( result == k_EVoiceResultNotRecording && g_bVoiceRecording ) + { + Voice_RecordStop(); + } + + g_pSoundServices->OnChangeVoiceStatus( -3, false ); + } + return cbCompressedWritten; + } + + IVoiceCodec *pCodec = g_pEncodeCodec; + if( g_pVoiceRecord && pCodec ) + { +#ifdef VOICE_VOX_ENABLE + static ConVarRef voice_vox( "voice_vox" ); +#endif // VOICE_VOX_ENABLE + + short tempData[8192]; + int samplesWanted = min(nCount/BYTES_PER_SAMPLE, (int)sizeof(tempData)/BYTES_PER_SAMPLE); + int gotten = g_pVoiceRecord->GetRecordedData(tempData, samplesWanted); + + // If they want to get the data from a file instead of the mic, use that. + if(g_pMicInputFileData) + { + double curtime = Plat_FloatTime(); + int nShouldGet = (curtime - g_MicStartTime) * Voice_SamplesPerSec(); + gotten = min(sizeof(tempData)/BYTES_PER_SAMPLE, + (size_t)min(nShouldGet, (g_nMicInputFileBytes - g_CurMicInputFileByte) / BYTES_PER_SAMPLE)); + memcpy(tempData, &g_pMicInputFileData[g_CurMicInputFileByte], gotten*BYTES_PER_SAMPLE); + g_CurMicInputFileByte += gotten * BYTES_PER_SAMPLE; + g_MicStartTime = curtime; + } +#ifdef VOICE_VOX_ENABLE + else if ( gotten && voice_vox.GetBool() == true ) + { + // If the voice data is essentially silent, don't transmit + short *pData = tempData; + int averageData = 0; + int minData = 16384; + int maxData = -16384; + for ( int i=0; i<gotten; ++i ) + { + short val = *pData; + averageData += val; + minData = min( val, minData ); + maxData = max( val, maxData ); + ++pData; + } + averageData /= gotten; + int deltaData = maxData - minData; + + if ( deltaData < voice_threshold.GetFloat() && maxData < voice_threshold.GetFloat() ) + { + // -3 signals that we're silent + g_pSoundServices->OnChangeVoiceStatus( -3, false ); + return 0; + } + } +#endif // VOICE_VOX_ENABLE + +#ifdef VOICE_SEND_RAW_TEST + int nCompressedBytes = min( gotten, nCount ); + for ( int i=0; i < nCompressedBytes; i++ ) + { + pchDest[i] = (char)(tempData[i] >> 8); + } +#else + int nCompressedBytes = pCodec->Compress((char*)tempData, gotten, pchDest, nCount, !!bFinal); +#endif + + // Write to our file buffers.. + if(g_pUncompressedFileData) + { + int nToWrite = min(gotten*BYTES_PER_SAMPLE, MAX_WAVEFILEDATA_LEN - g_nUncompressedDataBytes); + memcpy(&g_pUncompressedFileData[g_nUncompressedDataBytes], tempData, nToWrite); + g_nUncompressedDataBytes += nToWrite; + } +#ifdef VOICE_VOX_ENABLE + // -3 signals that we're talking + g_pSoundServices->OnChangeVoiceStatus( -3, (nCompressedBytes > 0) ); +#endif // VOICE_VOX_ENABLE + return nCompressedBytes; + } + else + { +#ifdef VOICE_VOX_ENABLE + // -3 signals that we're silent + g_pSoundServices->OnChangeVoiceStatus( -3, false ); +#endif // VOICE_VOX_ENABLE + return 0; + } +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +// Purpose: Assigns a channel to an entity by searching for either a channel +// already assigned to that entity or picking the least recently used +// channel. If the LRU channel is picked, it is flushed and all other +// channels are aged. +// Input : nEntity - entity number to assign to a channel. +// Output : A channel index to which the entity has been assigned. +//------------------------------------------------------------------------------ +int Voice_AssignChannel(int nEntity, bool bProximity) +{ + if(g_bInTweakMode) + return VOICE_CHANNEL_IN_TWEAK_MODE; + + // See if a channel already exists for this entity and if so, just return it. + int iFree = -1; + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + { + CVoiceChannel *pChannel = &g_VoiceChannels[i]; + + if(pChannel->m_iEntity == nEntity) + { + return i; + } + else if(pChannel->m_iEntity == -1 && ( pChannel->m_pVoiceCodec || g_bUsingSteamVoice ) ) + { + // Won't exist in steam voice mode + if ( pChannel->m_pVoiceCodec ) + { + pChannel->m_pVoiceCodec->ResetState(); + } + iFree = i; + break; + } + } + + // If they're all used, then don't allow them to make a new channel. + if(iFree == -1) + { + return VOICE_CHANNEL_ERROR; + } + + CVoiceChannel *pChannel = &g_VoiceChannels[iFree]; + pChannel->Init(nEntity); + pChannel->m_bProximity = bProximity; + VoiceSE_StartOverdrive(); + + return iFree; +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +// Purpose: Determines which channel has been assigened to a given entity. +// Input : nEntity - entity number. +// Output : The index of the channel assigned to the entity, VOICE_CHANNEL_ERROR +// if no channel is currently assigned to the given entity. +//------------------------------------------------------------------------------ +int Voice_GetChannel(int nEntity) +{ + for(int i=0; i < VOICE_NUM_CHANNELS; i++) + if(g_VoiceChannels[i].m_iEntity == nEntity) + return i; + + return VOICE_CHANNEL_ERROR; +} + + +double UpsampleIntoBuffer( + const short *pSrc, + int nSrcSamples, + CCircularBuffer *pBuffer, + double startFraction, + double rate) +{ + double maxFraction = nSrcSamples - 1; + + while(1) + { + if(startFraction >= maxFraction) + break; + + int iSample = (int)startFraction; + double frac = startFraction - floor(startFraction); + + double val1 = pSrc[iSample]; + double val2 = pSrc[iSample+1]; + short newSample = (short)(val1 + (val2 - val1) * frac); + pBuffer->Write(&newSample, sizeof(newSample)); + + startFraction += rate; + } + + return startFraction - floor(startFraction); +} + + +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +// Purpose: Adds received voice data to +// Input : +// Output : +//------------------------------------------------------------------------------ +int Voice_AddIncomingData(int nChannel, const char *pchData, int nCount, int iSequenceNumber) +{ + CVoiceChannel *pChannel; + + // If in tweak mode, we call this during Idle with -1 as the channel, so any channel data from the network + // gets rejected. + if(g_bInTweakMode) + { + if(nChannel == TWEAKMODE_CHANNELINDEX) + nChannel = 0; + else + return 0; + } + + if ( ( pChannel = GetVoiceChannel(nChannel)) == NULL || ( !g_bUsingSteamVoice && !pChannel->m_pVoiceCodec ) ) + { + return(0); + } + + pChannel->m_bStarved = false; // This only really matters if you call Voice_AddIncomingData between the time the mixer + // asks for data and Voice_Idle is called. + + // Decompress. + // @note Tom Bui: suggested destination buffer for Steam voice is 22kb + char decompressed[22528]; + +#ifdef VOICE_SEND_RAW_TEST + + int nDecompressed = nCount; + for ( int i=0; i < nDecompressed; i++ ) + ((short*)decompressed)[i] = pchData[i] << 8; + +#else + + int nDecompressed = 0; + if ( g_bUsingSteamVoice ) + { + uint32 nBytesWritten = 0; + EVoiceResult result = steamapicontext->SteamUser()->DecompressVoice( pchData, nCount, + decompressed, sizeof( decompressed ), + &nBytesWritten, Voice_SamplesPerSec() ); + if ( result == k_EVoiceResultOK ) + { + nDecompressed = nBytesWritten / BYTES_PER_SAMPLE; + } + } + else + { + nDecompressed = pChannel->m_pVoiceCodec->Decompress(pchData, nCount, decompressed, sizeof(decompressed)); + } + +#endif + + if ( g_bInTweakMode ) + { + short *data = (short *)decompressed; + g_VoiceTweakSpeakingVolume = 0; + + // Find the highest value + for ( int i=0; i<nDecompressed; ++i ) + { + g_VoiceTweakSpeakingVolume = max((int)abs(data[i]), g_VoiceTweakSpeakingVolume); + } + + // Smooth it out + g_VoiceTweakSpeakingVolume &= 0xFE00; + } + + pChannel->m_AutoGain.ProcessSamples((short*)decompressed, nDecompressed); + + // Upsample into the dest buffer. We could do this in a mixer but it complicates the mixer. + pChannel->m_LastFraction = UpsampleIntoBuffer( (short*)decompressed, + nDecompressed, + &pChannel->m_Buffer, + pChannel->m_LastFraction, + (double)Voice_SamplesPerSec()/g_VoiceSampleFormat.nSamplesPerSec ); + pChannel->m_LastSample = decompressed[nDecompressed]; + + // Write to our file buffer.. + if(g_pDecompressedFileData) + { + int nToWrite = min(nDecompressed*2, MAX_WAVEFILEDATA_LEN - g_nDecompressedDataBytes); + memcpy(&g_pDecompressedFileData[g_nDecompressedDataBytes], decompressed, nToWrite); + g_nDecompressedDataBytes += nToWrite; + } + + g_VoiceWriter.AddDecompressedData( pChannel, (const byte *)decompressed, nDecompressed * 2 ); + + if( voice_showincoming.GetInt() != 0 ) + { + Msg("Voice - %d incoming samples added to channel %d\n", nDecompressed, nChannel); + } + + return(nChannel); +} + + +#if DEAD +//------------------ Copyright (c) 1999 Valve, LLC. ---------------------------- +// Purpose: Flushes a given receive channel. +// Input : nChannel - index of channel to flush. +//------------------------------------------------------------------------------ +void Voice_FlushChannel(int nChannel) +{ + if ((nChannel < 0) || (nChannel >= VOICE_NUM_CHANNELS)) + { + Assert(false); + return; + } + + g_VoiceChannels[nChannel].m_Buffer.Flush(); +} +#endif + + +//------------------------------------------------------------------------------ +// IVoiceTweak implementation. +//------------------------------------------------------------------------------ + +int VoiceTweak_StartVoiceTweakMode() +{ + // If we're already in voice tweak mode, return an error. + if(g_bInTweakMode) + { + Assert(!"VoiceTweak_StartVoiceTweakMode called while already in tweak mode."); + return 0; + } + + if ( !g_pMixerControls && voice_enable.GetBool() ) + { + Voice_ForceInit(); + } + + if( !g_pMixerControls ) + return 0; + + Voice_EndAllChannels(); + Voice_RecordStart(NULL, NULL, NULL); + Voice_AssignChannel(TWEAKMODE_ENTITYINDEX, false ); + g_bInTweakMode = true; + InitMixerControls(); + + return 1; +} + +void VoiceTweak_EndVoiceTweakMode() +{ + if(!g_bInTweakMode) + { + Assert(!"VoiceTweak_EndVoiceTweakMode called when not in tweak mode."); + return; + } + + g_bInTweakMode = false; + Voice_RecordStop(); +} + +void VoiceTweak_SetControlFloat(VoiceTweakControl iControl, float flValue) +{ + if(!g_pMixerControls) + return; + + if(iControl == MicrophoneVolume) + { + g_pMixerControls->SetValue_Float(IMixerControls::MicVolume, flValue); + } + else if ( iControl == MicBoost ) + { + g_pMixerControls->SetValue_Float( IMixerControls::MicBoost, flValue ); + } + else if(iControl == OtherSpeakerScale) + { + voice_scale.SetValue( flValue ); + } +} + +void Voice_ForceInit() +{ + if ( g_pMixerControls || !voice_enable.GetBool() ) + { + // Nothing to do + return; + } + + // Lacking a better default, just peak at what the server's sv_voicecodec is set to + static ConVarRef sv_voicecodec( "sv_voicecodec" ); + if ( !Voice_InitWithDefault( sv_voicecodec.GetString() ) ) + { + // Try ultimate fallback + Voice_InitWithDefault( VOICE_FALLBACK_CODEC ); + } +} + +float VoiceTweak_GetControlFloat(VoiceTweakControl iControl) +{ + Voice_ForceInit(); + + if(!g_pMixerControls) + return 0; + + if(iControl == MicrophoneVolume) + { + float value = 1; + g_pMixerControls->GetValue_Float(IMixerControls::MicVolume, value); + return value; + } + else if(iControl == OtherSpeakerScale) + { + return voice_scale.GetFloat(); + } + else if(iControl == SpeakingVolume) + { + return g_VoiceTweakSpeakingVolume * 1.0f / 32768; + } + else if ( iControl == MicBoost ) + { + float flValue = 1; + g_pMixerControls->GetValue_Float( IMixerControls::MicBoost, flValue ); + return flValue; + } + else + { + return 1; + } +} + +bool VoiceTweak_IsStillTweaking() +{ + return g_bInTweakMode; +} + +void Voice_Spatialize( channel_t *channel ) +{ + if ( !g_bInTweakMode ) + return; + + Assert( channel->sfx ); + Assert( channel->sfx->pSource ); + Assert( channel->sfx->pSource->GetType() == CAudioSource::AUDIO_SOURCE_VOICE ); + + // Place the tweak mode sound back at the view entity + CVoiceChannel *pVoiceChannel = GetVoiceChannel( 0 ); + Assert( pVoiceChannel ); + if ( !pVoiceChannel ) + return; + + if ( pVoiceChannel->m_nSoundGuid != channel->guid ) + return; + + // No change + if ( g_pSoundServices->GetViewEntity() == pVoiceChannel->m_nViewEntityIndex ) + return; + + DevMsg( 1, "Voice_Spatialize changing voice tweak entity from %d to %d\n", pVoiceChannel->m_nViewEntityIndex, g_pSoundServices->GetViewEntity() ); + + pVoiceChannel->m_nViewEntityIndex = g_pSoundServices->GetViewEntity(); + channel->soundsource = pVoiceChannel->m_nViewEntityIndex; +} + +IVoiceTweak g_VoiceTweakAPI = +{ + VoiceTweak_StartVoiceTweakMode, + VoiceTweak_EndVoiceTweakMode, + VoiceTweak_SetControlFloat, + VoiceTweak_GetControlFloat, + VoiceTweak_IsStillTweaking, +}; + + diff --git a/engine/audio/private/voice_gain.cpp b/engine/audio/private/voice_gain.cpp new file mode 100644 index 0000000..bf14671 --- /dev/null +++ b/engine/audio/private/voice_gain.cpp @@ -0,0 +1,92 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + + +#include "audio_pch.h" +#include "minmax.h" +#include "voice_gain.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +CAutoGain::CAutoGain() +{ + Reset(128, 5.0f, 0.5f, 1); +} + + +void CAutoGain::Reset(int blockSize, float maxGain, float avgToMaxVal, float scale) +{ + m_BlockSize = blockSize; + m_MaxGain = maxGain; + m_AvgToMaxVal = avgToMaxVal; + + m_CurBlockOffset = 0; + m_CurTotal = 0; + m_CurMax = 0; + + m_CurrentGain = 1; + m_NextGain = 1; + + m_Scale = scale; + + m_GainMultiplier = 0; + m_FixedCurrentGain = 1 << AG_FIX_SHIFT; +} + + +void CAutoGain::ProcessSamples( + short *pSamples, + int nSamples) +{ + short *pCurPos = pSamples; + int nSamplesLeft = nSamples; + + // Continue until we hit the end of this block. + while(nSamplesLeft) + { + int nToProcess = min(nSamplesLeft, (m_BlockSize - m_CurBlockOffset)); + for(int iSample=0; iSample < nToProcess; iSample++) + { + // Update running totals.. + m_CurTotal += abs( pCurPos[iSample] ); + m_CurMax = max( m_CurMax, (int)abs( pCurPos[iSample] ) ); + + // Apply gain on this sample. + AGFixed gain = m_FixedCurrentGain + m_CurBlockOffset * m_GainMultiplier; + m_CurBlockOffset++; + + int newval = ((int)pCurPos[iSample] * gain) >> AG_FIX_SHIFT; + newval = min(32767, max(newval, -32768)); + pCurPos[iSample] = (short)newval; + } + pCurPos += nToProcess; + nSamplesLeft -= nToProcess; + + // Did we just end a block? Update our next gain. + if((m_CurBlockOffset % m_BlockSize) == 0) + { + // Now we've interpolated to our next gain, make it our current gain. + m_CurrentGain = m_NextGain * m_Scale; + m_FixedCurrentGain = (int)((double)m_CurrentGain * (1 << AG_FIX_SHIFT)); + + // Figure out the next gain (the gain we'll interpolate to). + int avg = m_CurTotal / m_BlockSize; + float modifiedMax = avg + (m_CurMax - avg) * m_AvgToMaxVal; + m_NextGain = min(32767.0f / modifiedMax, m_MaxGain) * m_Scale; + + // Setup the interpolation multiplier. + float fGainMultiplier = (m_NextGain - m_CurrentGain) / (m_BlockSize - 1); + m_GainMultiplier = (AGFixed)((double)fGainMultiplier * (1 << AG_FIX_SHIFT)); + + // Reset counters. + m_CurTotal = 0; + m_CurMax = 0; + m_CurBlockOffset = 0; + } + } +} diff --git a/engine/audio/private/voice_gain.h b/engine/audio/private/voice_gain.h new file mode 100644 index 0000000..cd614e0 --- /dev/null +++ b/engine/audio/private/voice_gain.h @@ -0,0 +1,62 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOICE_GAIN_H +#define VOICE_GAIN_H +#pragma once + + +// ----------------------------------------------------------------------- // +// CAutoGain is fed samples and figures out a gain to apply to blocks of samples. + +// Right now, this class applies gain one block behind. The assumption is that the blocks are +// small enough that gain settings for one block will usually be right for the next block. + +// The ideal way to implement this class would be to have a delay the size of a block +// so it can apply the right gain to the actual block it was calculated for. +// ----------------------------------------------------------------------- // +class CAutoGain +{ +public: + + CAutoGain(); + + // maxGain and avgToMaxVal are used to derive the gain amount for each block of samples. + // All samples are scaled by scale. + void Reset(int blockSize, float maxGain, float avgToMaxVal, float scale); + + // Process the specified samples and apply gain to them. + void ProcessSamples( + short *pSamples, + int nSamples); + +private: + + enum {AG_FIX_SHIFT=7}; + typedef long AGFixed; + + // Parameters affecting the algorithm. + int m_BlockSize; // Derive gain from blocks of this size. + float m_MaxGain; + float m_AvgToMaxVal; + + // These are calculated as samples are passed in. + int m_CurBlockOffset; + int m_CurTotal; // Total of sample values in current block. + int m_CurMax; // Highest (absolute) sample value. + + float m_Scale; // All samples are scaled by this amount. + + float m_CurrentGain; // Gain at sample 0 in this block. + float m_NextGain; // Gain at the last sample in this block. + + AGFixed m_FixedCurrentGain; // Fixed-point m_CurrentGain. + AGFixed m_GainMultiplier; // (m_NextGain - m_CurrentGain) / (m_BlockSize - 1). +}; + + +#endif // VOICE_GAIN_H diff --git a/engine/audio/private/voice_mixer_controls.cpp b/engine/audio/private/voice_mixer_controls.cpp new file mode 100644 index 0000000..44c6a94 --- /dev/null +++ b/engine/audio/private/voice_mixer_controls.cpp @@ -0,0 +1,503 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + + +#include "audio_pch.h" +#include "voice_mixer_controls.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +// NOTE: Vista deprecated these APIs +// Under vista these settings are per-session (not persistent) +// The correct method is to use the AudioEndpoint COM objects to manage controls like this +// The interface is not 1:1 so for now we'll just back the state with convars and reapply it +// on init of the mixer controls. In the future when XP is no longer the majority of our user base +// we should revisit this and move to the new API. +// http://msdn.microsoft.com/en-us/library/aa964574(VS.85).aspx + + +class CMixerControls : public IMixerControls +{ +public: + CMixerControls(); + virtual ~CMixerControls(); + + virtual bool GetValue_Float(Control iControl, float &value); + virtual bool SetValue_Float(Control iControl, float value); + virtual bool SelectMicrophoneForWaveInput(); + + +private: + bool Init(); + void Term(); + + void Clear(); + + bool GetControlOption_Bool(DWORD dwControlID, DWORD cMultipleItems, bool &bValue); + bool SetControlOption_Bool(DWORD dwControlID, DWORD cMultipleItems, const bool bValue); + + bool GetControlOption_Unsigned(DWORD dwControlID, DWORD cMultipleItems, DWORD &value); + bool SetControlOption_Unsigned(DWORD dwControlID, DWORD cMultipleItems, const DWORD value); + + bool GetLineControls( DWORD dwLineID, MIXERCONTROL *controls, DWORD nControls ); + void FindMicSelectControl( DWORD dwLineID, DWORD nControls ); + + +private: + HMIXER m_hMixer; + + class ControlInfo + { + public: + DWORD m_dwControlID; + DWORD m_cMultipleItems; + bool m_bFound; + }; + + DWORD m_dwMicSelectControlID; + DWORD m_dwMicSelectMultipleItems; + DWORD m_dwMicSelectControlType; + DWORD m_dwMicSelectIndex; + + // Info about the controls we found. + ControlInfo m_ControlInfos[NumControls]; +}; + + + +CMixerControls::CMixerControls() +{ + m_dwMicSelectControlID = 0xFFFFFFFF; + + Clear(); + Init(); +} + +CMixerControls::~CMixerControls() +{ + Term(); +} + +bool CMixerControls::Init() +{ + Term(); + + + MMRESULT mmr; + + bool bFoundMixer = false; + bool bFoundConnectionWithMicVolume = false; + + CUtlVectorFixedGrowable<MIXERCONTROL, 64> controls; + // Iterate over all the devices + // This is done in reverse so the 0th device is our fallback if none of them had the correct MicVolume control + for ( int iDevice = static_cast<int>( mixerGetNumDevs() ) - 1; iDevice >= 0 && !bFoundConnectionWithMicVolume; --iDevice ) + { + // Open the mixer. + mmr = mixerOpen(&m_hMixer, (DWORD)iDevice, 0, 0, 0 ); + if(mmr != MMSYSERR_NOERROR) + { + continue; + } + + // Iterate over each destination line, looking for Play Controls. + MIXERCAPS mxcaps; + mmr = mixerGetDevCaps((UINT)m_hMixer, &mxcaps, sizeof(mxcaps)); + if(mmr != MMSYSERR_NOERROR) + { + continue; + } + + bFoundMixer = true; + + for(UINT u = 0; u < mxcaps.cDestinations; u++) + { + MIXERLINE recordLine; + recordLine.cbStruct = sizeof(recordLine); + recordLine.dwDestination = u; + mmr = mixerGetLineInfo((HMIXEROBJ)m_hMixer, &recordLine, MIXER_GETLINEINFOF_DESTINATION); + if(mmr != MMSYSERR_NOERROR) + continue; + + + // Go through the controls that aren't attached to a specific src connection. + // We're looking for the checkbox that enables the user's microphone for waveIn. + if( recordLine.dwComponentType == MIXERLINE_COMPONENTTYPE_DST_WAVEIN ) + { + FindMicSelectControl( recordLine.dwLineID, recordLine.cControls ); + } + + + // Now iterate over each connection (things like wave out, microphone, speaker, CD audio), looking for Microphone. + UINT cConnections = (UINT)recordLine.cConnections; + for (UINT v = 0; v < cConnections; v++) + { + MIXERLINE micLine; + micLine.cbStruct = sizeof(micLine); + micLine.dwDestination = u; + micLine.dwSource = v; + + mmr = mixerGetLineInfo((HMIXEROBJ)m_hMixer, &micLine, MIXER_GETLINEINFOF_SOURCE); + if(mmr != MMSYSERR_NOERROR) + continue; + + // Now look at all the controls (volume, mute, boost, etc). + controls.RemoveAll(); + controls.SetCount(micLine.cControls); + if( !GetLineControls( micLine.dwLineID, controls.Base(), micLine.cControls ) ) + continue; + + for(UINT i=0; i < micLine.cControls; i++) + { + MIXERCONTROL *pControl = &controls[i]; + + if(micLine.dwComponentType == MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE) + { + if( pControl->dwControlType == MIXERCONTROL_CONTROLTYPE_ONOFF && + ( + strstr(pControl->szShortName, "Gain") || + strstr(pControl->szShortName, "Boos") || + strstr(pControl->szShortName, "+20d") + ) + ) + { + // This is the (record) boost option. + m_ControlInfos[MicBoost].m_bFound = true; + m_ControlInfos[MicBoost].m_dwControlID = pControl->dwControlID; + m_ControlInfos[MicBoost].m_cMultipleItems = pControl->cMultipleItems; + } + + if(recordLine.dwComponentType == MIXERLINE_COMPONENTTYPE_DST_SPEAKERS && + pControl->dwControlType == MIXERCONTROL_CONTROLTYPE_MUTE) + { + // This is the mute button. + m_ControlInfos[MicMute].m_bFound = true; + m_ControlInfos[MicMute].m_dwControlID = pControl->dwControlID; + m_ControlInfos[MicMute].m_cMultipleItems = pControl->cMultipleItems; + } + + if(recordLine.dwComponentType == MIXERLINE_COMPONENTTYPE_DST_WAVEIN && + pControl->dwControlType == MIXERCONTROL_CONTROLTYPE_VOLUME) + { + // This is the mic input level. + m_ControlInfos[MicVolume].m_bFound = true; + m_ControlInfos[MicVolume].m_dwControlID = pControl->dwControlID; + m_ControlInfos[MicVolume].m_cMultipleItems = pControl->cMultipleItems; + + // We found a good recording device and can stop looking throught the available devices + bFoundConnectionWithMicVolume = true; + } + } + } + } + } + } + + if ( !bFoundMixer ) + { + // Failed to find any mixer (MixVolume or not) + Term(); + return false; + } + + return true; +} + +void CMixerControls::Term() +{ + if(m_hMixer) + { + mixerClose(m_hMixer); + m_hMixer = 0; + } + + Clear(); +} + + + +bool CMixerControls::GetValue_Float( Control iControl, float &flValue ) +{ + if( iControl < 0 || iControl >= NumControls || !m_ControlInfos[iControl].m_bFound ) + return false; + + if(iControl == MicBoost || iControl == MicMute) + { + bool bValue = false; + bool ret = GetControlOption_Bool(m_ControlInfos[iControl].m_dwControlID, m_ControlInfos[iControl].m_cMultipleItems, bValue); + flValue = (float)bValue; + return ret; + } + else if(iControl == MicVolume) + { + DWORD dwValue = (DWORD)0; + if(GetControlOption_Unsigned(m_ControlInfos[iControl].m_dwControlID, m_ControlInfos[iControl].m_cMultipleItems, dwValue)) + { + flValue = dwValue / 65535.0f; + return true; + } + } + + return false; +} + + +bool CMixerControls::SetValue_Float(Control iControl, float flValue ) +{ + if(iControl < 0 || iControl >= NumControls || !m_ControlInfos[iControl].m_bFound) + return false; + + if(iControl == MicBoost || iControl == MicMute) + { + bool bValue = !!flValue; + return SetControlOption_Bool(m_ControlInfos[iControl].m_dwControlID, m_ControlInfos[iControl].m_cMultipleItems, bValue); + } + else if(iControl == MicVolume) + { + DWORD dwValue = (DWORD)(flValue * 65535.0f); + return SetControlOption_Unsigned(m_ControlInfos[iControl].m_dwControlID, m_ControlInfos[iControl].m_cMultipleItems, dwValue); + } + return false; +} + + +bool CMixerControls::SelectMicrophoneForWaveInput() +{ + if( m_dwMicSelectControlID == 0xFFFFFFFF ) + return false; + + MIXERCONTROLDETAILS_BOOLEAN *pmxcdSelectValue = + (MIXERCONTROLDETAILS_BOOLEAN*)_alloca( sizeof(MIXERCONTROLDETAILS_BOOLEAN) * m_dwMicSelectMultipleItems ); + + MIXERCONTROLDETAILS mxcd; + mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS); + mxcd.dwControlID = m_dwMicSelectControlID; + mxcd.cChannels = 1; + mxcd.cMultipleItems = m_dwMicSelectMultipleItems; + mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_BOOLEAN); + mxcd.paDetails = pmxcdSelectValue; + if (mixerGetControlDetails(reinterpret_cast<HMIXEROBJ>(m_hMixer), + &mxcd, + MIXER_OBJECTF_HMIXER | + MIXER_GETCONTROLDETAILSF_VALUE) + == MMSYSERR_NOERROR) + { + // MUX restricts the line selection to one source line at a time. + if( m_dwMicSelectControlType == MIXERCONTROL_CONTROLTYPE_MUX ) + { + ZeroMemory(pmxcdSelectValue, + m_dwMicSelectMultipleItems * sizeof(MIXERCONTROLDETAILS_BOOLEAN)); + } + + // set the Microphone value + pmxcdSelectValue[m_dwMicSelectIndex].fValue = 1; + + mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS); + mxcd.dwControlID = m_dwMicSelectControlID; + mxcd.cChannels = 1; + mxcd.cMultipleItems = m_dwMicSelectMultipleItems; + mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_BOOLEAN); + mxcd.paDetails = pmxcdSelectValue; + if (mixerSetControlDetails(reinterpret_cast<HMIXEROBJ>(m_hMixer), + &mxcd, + MIXER_OBJECTF_HMIXER | + MIXER_SETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR) + { + return true; + } + } + + + return false; +} + + +void CMixerControls::Clear() +{ + m_hMixer = 0; + memset(m_ControlInfos, 0, sizeof(m_ControlInfos)); +} + +bool CMixerControls::GetControlOption_Bool(DWORD dwControlID, DWORD cMultipleItems, bool &bValue) +{ + MIXERCONTROLDETAILS details; + MIXERCONTROLDETAILS_BOOLEAN controlValue; + + details.cbStruct = sizeof(details); + details.dwControlID = dwControlID; + details.cChannels = 1; // uniform.. + details.cMultipleItems = cMultipleItems; + details.cbDetails = sizeof(MIXERCONTROLDETAILS_BOOLEAN); + details.paDetails = &controlValue; + + MMRESULT mmr = mixerGetControlDetails((HMIXEROBJ)m_hMixer, &details, 0L); + if(mmr == MMSYSERR_NOERROR) + { + bValue = !!controlValue.fValue; + return true; + } + else + { + return false; + } +} + +bool CMixerControls::SetControlOption_Bool(DWORD dwControlID, DWORD cMultipleItems, const bool bValue) +{ + MIXERCONTROLDETAILS details; + MIXERCONTROLDETAILS_BOOLEAN controlValue; + + details.cbStruct = sizeof(details); + details.dwControlID = dwControlID; + details.cChannels = 1; // uniform.. + details.cMultipleItems = cMultipleItems; + details.cbDetails = sizeof(MIXERCONTROLDETAILS_BOOLEAN); + details.paDetails = &controlValue; + + controlValue.fValue = bValue; + + MMRESULT mmr = mixerSetControlDetails((HMIXEROBJ)m_hMixer, &details, 0L); + return mmr == MMSYSERR_NOERROR; +} + +bool CMixerControls::GetControlOption_Unsigned(DWORD dwControlID, DWORD cMultipleItems, DWORD &value) +{ + MIXERCONTROLDETAILS details; + MIXERCONTROLDETAILS_UNSIGNED controlValue; + + details.cbStruct = sizeof(details); + details.dwControlID = dwControlID; + details.cChannels = 1; // uniform.. + details.cMultipleItems = cMultipleItems; + details.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); + details.paDetails = &controlValue; + + MMRESULT mmr = mixerGetControlDetails((HMIXEROBJ)m_hMixer, &details, 0L); + if(mmr == MMSYSERR_NOERROR) + { + value = controlValue.dwValue; + return true; + } + else + { + return false; + } +} + +bool CMixerControls::SetControlOption_Unsigned(DWORD dwControlID, DWORD cMultipleItems, const DWORD value) +{ + MIXERCONTROLDETAILS details; + MIXERCONTROLDETAILS_UNSIGNED controlValue; + + details.cbStruct = sizeof(details); + details.dwControlID = dwControlID; + details.cChannels = 1; // uniform.. + details.cMultipleItems = cMultipleItems; + details.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED); + details.paDetails = &controlValue; + + controlValue.dwValue = value; + + MMRESULT mmr = mixerSetControlDetails((HMIXEROBJ)m_hMixer, &details, 0L); + return mmr == MMSYSERR_NOERROR; +} + + +bool CMixerControls::GetLineControls( DWORD dwLineID, MIXERCONTROL *controls, DWORD nControls ) +{ + MIXERLINECONTROLS mxlc; + + mxlc.cbStruct = sizeof(mxlc); + mxlc.dwLineID = dwLineID; + mxlc.cControls = nControls; + mxlc.cbmxctrl = sizeof(MIXERCONTROL); + mxlc.pamxctrl = controls; + + MMRESULT mmr = mixerGetLineControls((HMIXEROBJ)m_hMixer, &mxlc, MIXER_GETLINECONTROLSF_ALL); + return mmr == MMSYSERR_NOERROR; +} + + +void CMixerControls::FindMicSelectControl( DWORD dwLineID, DWORD nControls ) +{ + m_dwMicSelectControlID = 0xFFFFFFFF; + + MIXERCONTROL *recControls = (MIXERCONTROL*)_alloca( sizeof(MIXERCONTROL) * nControls ); + if( !GetLineControls( dwLineID, recControls, nControls ) ) + return; + + for( UINT iRecControl=0; iRecControl < nControls; iRecControl++ ) + { + if( recControls[iRecControl].dwControlType == MIXERCONTROL_CONTROLTYPE_MIXER || + recControls[iRecControl].dwControlType == MIXERCONTROL_CONTROLTYPE_MUX ) + { + m_dwMicSelectControlID = recControls[iRecControl].dwControlID; + m_dwMicSelectControlType = recControls[iRecControl].dwControlType; + m_dwMicSelectMultipleItems = recControls[iRecControl].cMultipleItems; + m_dwMicSelectIndex = iRecControl; + + // Get the index of the one that selects the mic. + MIXERCONTROLDETAILS_LISTTEXT *pmxcdSelectText = + (MIXERCONTROLDETAILS_LISTTEXT*)_alloca( sizeof(MIXERCONTROLDETAILS_LISTTEXT) * m_dwMicSelectMultipleItems ); + + MIXERCONTROLDETAILS mxcd; + mxcd.cbStruct = sizeof(MIXERCONTROLDETAILS); + mxcd.dwControlID = m_dwMicSelectControlID; + mxcd.cChannels = 1; + mxcd.cMultipleItems = m_dwMicSelectMultipleItems; + mxcd.cbDetails = sizeof(MIXERCONTROLDETAILS_LISTTEXT); + mxcd.paDetails = pmxcdSelectText; + + if (mixerGetControlDetails((HMIXEROBJ)m_hMixer, + &mxcd, + MIXER_OBJECTF_HMIXER | + MIXER_GETCONTROLDETAILSF_LISTTEXT) == MMSYSERR_NOERROR) + { + // determine which controls the Microphone source line + for (DWORD dwi = 0; dwi < m_dwMicSelectMultipleItems; dwi++) + { + // get the line information + MIXERLINE mxl; + mxl.cbStruct = sizeof(MIXERLINE); + mxl.dwLineID = pmxcdSelectText[dwi].dwParam1; + + if (mixerGetLineInfo((HMIXEROBJ)m_hMixer, + &mxl, + MIXER_OBJECTF_HMIXER | + MIXER_GETLINEINFOF_LINEID) == MMSYSERR_NOERROR && + mxl.dwComponentType == MIXERLINE_COMPONENTTYPE_SRC_MICROPHONE) + { + // found, dwi is the index. + m_dwMicSelectIndex = dwi; + break; + } + } + } + + break; + } + } +} + + +IMixerControls* g_pMixerControls = NULL; +void InitMixerControls() +{ + if ( !g_pMixerControls ) + { + g_pMixerControls = new CMixerControls; + } +} + +void ShutdownMixerControls() +{ + delete g_pMixerControls; + g_pMixerControls = NULL; +} + diff --git a/engine/audio/private/voice_mixer_controls.h b/engine/audio/private/voice_mixer_controls.h new file mode 100644 index 0000000..295ac88 --- /dev/null +++ b/engine/audio/private/voice_mixer_controls.h @@ -0,0 +1,47 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef MIXER_CONTROLS_H +#define MIXER_CONTROLS_H +#pragma once + + +abstract_class IMixerControls +{ + +public: + virtual ~IMixerControls() {} + enum Control + { + // Microphone boost is a boolean switch that sound cards support which boosts the input signal by about +20dB. + // If this isn't on, the mic is usually way too quiet. + MicBoost=0, + + // Volume values are 0-1. + MicVolume, + + // Mic playback muting. You usually want this set to false, otherwise the sound card echoes whatever you say into the mic. + MicMute, + + NumControls + }; + + virtual bool GetValue_Float(Control iControl, float &value) = 0; + virtual bool SetValue_Float(Control iControl, float value) = 0; + + // Apps like RealJukebox will switch the waveIn input to use CD audio + // rather than the microphone. This should be called at startup to set it back. + virtual bool SelectMicrophoneForWaveInput() = 0; +}; + +extern IMixerControls *g_pMixerControls; +// Allocates a set of mixer controls. +void InitMixerControls(); +void ShutdownMixerControls(); + + +#endif // MIXER_CONTROLS_H diff --git a/engine/audio/private/voice_mixer_controls_openal.cpp b/engine/audio/private/voice_mixer_controls_openal.cpp new file mode 100644 index 0000000..832815c --- /dev/null +++ b/engine/audio/private/voice_mixer_controls_openal.cpp @@ -0,0 +1,286 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + + +#ifdef OSX +#include <Carbon/Carbon.h> +#include <CoreAudio/CoreAudio.h> +#endif + +#include "tier0/platform.h" +#include "ivoicerecord.h" +#include "voice_mixer_controls.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + + +#ifndef OSX + + +class CMixerControls : public IMixerControls +{ +public: + CMixerControls() {} + virtual ~CMixerControls() {} + + virtual void Release() {} + virtual bool GetValue_Float(Control iControl, float &value ) {return false;} + virtual bool SetValue_Float(Control iControl, float value) {return false;} + virtual bool SelectMicrophoneForWaveInput() {return false;} + virtual const char *GetMixerName() {return "Linux"; } + +private: +}; + +IMixerControls* g_pMixerControls = NULL; +void InitMixerControls() +{ + if ( !g_pMixerControls ) + { + g_pMixerControls = new CMixerControls; + } +} + +void ShutdownMixerControls() +{ + delete g_pMixerControls; + g_pMixerControls = NULL; +} + +#elif defined(OSX) + +class CMixerControls : public IMixerControls +{ +public: + CMixerControls(); + virtual ~CMixerControls(); + + virtual void Release(); + virtual bool GetValue_Float(Control iControl, float &value); + virtual bool SetValue_Float(Control iControl, float value); + virtual bool SelectMicrophoneForWaveInput(); + virtual const char *GetMixerName(); + +private: + AudioObjectID GetDefaultInputDevice(); + char *m_szMixerName; + AudioObjectID m_theDefaultDeviceID; +}; + + +CMixerControls::CMixerControls() +{ + m_szMixerName = NULL; + + m_theDefaultDeviceID = GetDefaultInputDevice(); + + OSStatus theStatus; + UInt32 outSize = sizeof(UInt32); + theStatus = AudioDeviceGetPropertyInfo( m_theDefaultDeviceID, + 0, + TRUE, + kAudioDevicePropertyDeviceName, + &outSize, + NULL); + if ( theStatus == noErr ) + { + m_szMixerName = (char *)malloc( outSize*sizeof(char)); + + theStatus = AudioDeviceGetProperty( m_theDefaultDeviceID, + 0, + TRUE, + kAudioDevicePropertyDeviceName, + &outSize, + m_szMixerName); + + if ( theStatus != noErr ) + { + free( m_szMixerName ); + m_szMixerName = NULL; + } + } +} + +CMixerControls::~CMixerControls() +{ + if ( m_szMixerName ) + free( m_szMixerName ); +} + +void CMixerControls::Release() +{ +} + +bool CMixerControls::SelectMicrophoneForWaveInput() +{ + return true; // not needed +} + + +const char *CMixerControls::GetMixerName() +{ + return m_szMixerName; +} + + +bool CMixerControls::GetValue_Float(Control iControl, float &value) +{ + switch( iControl) + { + case MicBoost: + { + value = 0.0f; + return true; + } + case MicVolume: + { + OSStatus theError = noErr; + for ( int iChannel = 0; iChannel < 3; iChannel++ ) + { + // scan the channel list until you find a channel set to non-zero, then use that + Float32 theVolume = 0; + UInt32 theSize = sizeof(Float32); + AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyVolumeScalar, kAudioDevicePropertyScopeInput, iChannel }; + + theError = AudioObjectGetPropertyData(m_theDefaultDeviceID, + &theAddress, + 0, + NULL, + &theSize, + &theVolume); + value = theVolume; + if ( theError == noErr && theVolume != 0.0f ) + break; + } + + return theError == noErr; + } + + case MicMute: + // Mic playback muting. You usually want this set to false, otherwise the sound card echoes whatever you say into the mic. + { + Float32 theMute = 0; + UInt32 theSize = sizeof(Float32); + AudioObjectPropertyAddress theAddress = { kAudioDevicePropertyMute, kAudioDevicePropertyScopeInput, 1 }; + + OSStatus theError = AudioObjectGetPropertyData(m_theDefaultDeviceID, + &theAddress, + 0, + NULL, + &theSize, + &theMute); + value = theMute; + return theError == noErr; + } + default: + assert( !"Invalid Control type" ); + value = 0.0f; + return false; + }; +} + + +bool CMixerControls::SetValue_Float(Control iControl, float value) +{ + switch( iControl) + { + case MicBoost: + { + return false; + } + case MicVolume: + { + if ( value <= 0.0 ) + return false; // don't let the volume be set to zero + + Float32 theVolume = value; + UInt32 size = sizeof(Float32); + Boolean canset = false; + AudioObjectID defaultInputDevice = m_theDefaultDeviceID; + + size = sizeof(canset); + OSStatus err = AudioDeviceGetPropertyInfo( defaultInputDevice, 0, true, kAudioDevicePropertyVolumeScalar, &size, &canset); + if(err==noErr && canset==true) + { + size = sizeof(theVolume); + err = AudioDeviceSetProperty( defaultInputDevice, NULL, 0, true, kAudioDevicePropertyVolumeScalar, size, &theVolume); + return err==noErr; + } + + // try seperate channels + // get channels + UInt32 channels[2]; + size = sizeof(channels); + err = AudioDeviceGetProperty(defaultInputDevice, 0, true, kAudioDevicePropertyPreferredChannelsForStereo, &size,&channels); + if(err!=noErr) + return false; + + // set volume + size = sizeof(float); + err = AudioDeviceSetProperty(defaultInputDevice, 0, channels[0], true, kAudioDevicePropertyVolumeScalar, size, &theVolume); + //AssertMsg1( noErr==err, "error setting volume of channel %d\n",(int)channels[0]); + err = AudioDeviceSetProperty(defaultInputDevice, 0, channels[1], true, kAudioDevicePropertyVolumeScalar, size, &theVolume); + //AssertMsg1( noErr==err, "error setting volume of channel %d\n",(int)channels[1]); + + return err == noErr; + + } + case MicMute: + // Mic playback muting. You usually want this set to false, otherwise the sound card echoes whatever you say into the mic. + { + Float32 theMute = value; + UInt32 theMuteSize = sizeof(Float32); + OSStatus theError = paramErr; + theError = AudioDeviceSetProperty( m_theDefaultDeviceID, + NULL, + 0, + TRUE, + kAudioDevicePropertyMute, + theMuteSize, + &theMute); + return theError == noErr; + } + default: + assert( !"Invalid Control type" ); + return false; + }; +} + + +AudioObjectID CMixerControls::GetDefaultInputDevice() +{ + AudioObjectID theDefaultDeviceID = kAudioObjectUnknown; + AudioObjectPropertyAddress theDefaultDeviceAddress = { kAudioHardwarePropertyDefaultInputDevice, kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyElementMaster }; + UInt32 theDefaultDeviceSize = sizeof(AudioObjectID); + OSStatus theError = AudioObjectGetPropertyData (kAudioObjectSystemObject, &theDefaultDeviceAddress, 0, NULL, &theDefaultDeviceSize, &theDefaultDeviceID); + return theDefaultDeviceID; +} + + +IMixerControls* g_pMixerControls = NULL; +void InitMixerControls() +{ + if ( !g_pMixerControls ) + { + g_pMixerControls = new CMixerControls; + } +} + +void ShutdownMixerControls() +{ + delete g_pMixerControls; + g_pMixerControls = NULL; +} + + + +#else +#error +#endif + diff --git a/engine/audio/private/voice_record_dsound.cpp b/engine/audio/private/voice_record_dsound.cpp new file mode 100644 index 0000000..70fc2b6 --- /dev/null +++ b/engine/audio/private/voice_record_dsound.cpp @@ -0,0 +1,400 @@ +//========= Copyright � 1996-2005, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// + +// This module implements the voice record and compression functions + +#include "audio_pch.h" +#if !defined( _X360 ) +#include "dsound.h" +#endif +#include <assert.h> +#include "voice.h" +#include "tier0/vcrmode.h" +#include "ivoicerecord.h" + +#if defined( _X360 ) +#include "xbox/xbox_win32stubs.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + + +// ------------------------------------------------------------------------------ +// Globals. +// ------------------------------------------------------------------------------ + +typedef HRESULT (WINAPI *DirectSoundCaptureCreateFn)(const GUID FAR *lpGUID, LPDIRECTSOUNDCAPTURE *pCapture, LPUNKNOWN pUnkOuter); + + + +// ------------------------------------------------------------------------------ +// Static helpers +// ------------------------------------------------------------------------------ + + + +// ------------------------------------------------------------------------------ +// VoiceRecord_DSound +// ------------------------------------------------------------------------------ + +class VoiceRecord_DSound : public IVoiceRecord +{ +protected: + + virtual ~VoiceRecord_DSound(); + + +// IVoiceRecord. +public: + + VoiceRecord_DSound(); + virtual void Release(); + + virtual bool RecordStart(); + virtual void RecordStop(); + + // Initialize. The format of the data we expect from the provider is + // 8-bit signed mono at the specified sample rate. + virtual bool Init(int sampleRate); + + virtual void Idle(); + + // Get the most recent N samples. + virtual int GetRecordedData(short *pOut, int nSamplesWanted); + +private: + void Term(); // Delete members. + void Clear(); // Clear members. + void UpdateWrapping(); + + inline DWORD NumCaptureBufferBytes() {return m_nCaptureBufferBytes;} + + +private: + HINSTANCE m_hInstDS; + + LPDIRECTSOUNDCAPTURE m_pCapture; + LPDIRECTSOUNDCAPTUREBUFFER m_pCaptureBuffer; + + // How many bytes our capture buffer has. + DWORD m_nCaptureBufferBytes; + + // We need to know when the capture buffer loops, so we install an event and + // update this in the event. + DWORD m_WrapOffset; + HANDLE m_hWrapEvent; + + // This is our (unwrapped) position that tells how much data we've given to the app. + DWORD m_LastReadPos; +}; + + + +VoiceRecord_DSound::VoiceRecord_DSound() +{ + Clear(); +} + + +VoiceRecord_DSound::~VoiceRecord_DSound() +{ + Term(); +} + + +void VoiceRecord_DSound::Release() +{ + delete this; +} + + +bool VoiceRecord_DSound::RecordStart() +{ + //When we start recording we want to make sure we don't provide any audio + //that occurred before now. So set m_LastReadPos to the current + //read position of the audio device + if (m_pCaptureBuffer == NULL) + { + return false; + } + + Idle(); + + DWORD dwStatus; + HRESULT hr = m_pCaptureBuffer->GetStatus(&dwStatus); + if (FAILED(hr) || !(dwStatus & DSCBSTATUS_CAPTURING)) + return false; + + DWORD dwReadPos; + hr = m_pCaptureBuffer->GetCurrentPosition(NULL, &dwReadPos); + if (!FAILED(hr)) + { + m_LastReadPos = dwReadPos + m_WrapOffset; + } + + return true; +} + + +void VoiceRecord_DSound::RecordStop() +{ +} + +static bool IsRunningWindows7() +{ + if ( IsPC() ) + { + OSVERSIONINFOEX osvi; + ZeroMemory(&osvi, sizeof(OSVERSIONINFOEX)); + osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX); + + if ( GetVersionEx ((OSVERSIONINFO *)&osvi) ) + { + if ( osvi.dwMajorVersion > 6 || (osvi.dwMajorVersion == 6 && osvi.dwMinorVersion >= 1) ) + return true; + } + } + return false; +} + +bool VoiceRecord_DSound::Init(int sampleRate) +{ + HRESULT hr; + DSCBUFFERDESC dscDesc; + DirectSoundCaptureCreateFn createFn; + + + Term(); + + + WAVEFORMATEX recordFormat = + { + WAVE_FORMAT_PCM, // wFormatTag + 1, // nChannels + (uint32)sampleRate, // nSamplesPerSec + (uint32)sampleRate*2, // nAvgBytesPerSec + 2, // nBlockAlign + 16, // wBitsPerSample + sizeof(WAVEFORMATEX) // cbSize + }; + + + + // Load the DSound DLL. + m_hInstDS = LoadLibrary("dsound.dll"); + if(!m_hInstDS) + goto HandleError; + + createFn = (DirectSoundCaptureCreateFn)GetProcAddress(m_hInstDS, "DirectSoundCaptureCreate"); + if(!createFn) + goto HandleError; + + const GUID FAR *pGuid = &DSDEVID_DefaultVoiceCapture; + if ( IsRunningWindows7() ) + { + pGuid = NULL; + } + hr = createFn(pGuid, &m_pCapture, NULL); + if(FAILED(hr)) + goto HandleError; + + // Create the capture buffer. + memset(&dscDesc, 0, sizeof(dscDesc)); + dscDesc.dwSize = sizeof(dscDesc); + dscDesc.dwFlags = 0; + dscDesc.dwBufferBytes = recordFormat.nAvgBytesPerSec; + dscDesc.lpwfxFormat = &recordFormat; + + hr = m_pCapture->CreateCaptureBuffer(&dscDesc, &m_pCaptureBuffer, NULL); + if(FAILED(hr)) + goto HandleError; + + + // Figure out how many bytes we got in our capture buffer. + DSCBCAPS caps; + memset(&caps, 0, sizeof(caps)); + caps.dwSize = sizeof(caps); + + hr = m_pCaptureBuffer->GetCaps(&caps); + if(FAILED(hr)) + goto HandleError; + + m_nCaptureBufferBytes = caps.dwBufferBytes; + + + // Set it up so we get notification when the buffer wraps. + m_hWrapEvent = CreateEvent(NULL, FALSE, FALSE, NULL); + if(!m_hWrapEvent) + goto HandleError; + + DSBPOSITIONNOTIFY dsbNotify; + dsbNotify.dwOffset = dscDesc.dwBufferBytes - 1; + dsbNotify.hEventNotify = m_hWrapEvent; + + // Get the IDirectSoundNotify interface. + LPDIRECTSOUNDNOTIFY pNotify; + hr = m_pCaptureBuffer->QueryInterface(IID_IDirectSoundNotify, (void**)&pNotify); + if(FAILED(hr)) + goto HandleError; + + hr = pNotify->SetNotificationPositions(1, &dsbNotify); + pNotify->Release(); + if(FAILED(hr)) + goto HandleError; + + // Start capturing. + hr = m_pCaptureBuffer->Start(DSCBSTART_LOOPING); + if(FAILED(hr)) + return false; + + return true; + + +HandleError:; + Term(); + return false; +} + + +void VoiceRecord_DSound::Term() +{ + if(m_pCaptureBuffer) + m_pCaptureBuffer->Release(); + + if(m_pCapture) + m_pCapture->Release(); + + if(m_hWrapEvent) + DeleteObject(m_hWrapEvent); + + if(m_hInstDS) + { + FreeLibrary(m_hInstDS); + m_hInstDS = NULL; + } + + Clear(); +} + + +void VoiceRecord_DSound::Clear() +{ + m_pCapture = NULL; + m_pCaptureBuffer = NULL; + m_WrapOffset = 0; + m_LastReadPos = 0; + m_hWrapEvent = NULL; + m_hInstDS = NULL; +} + + +void VoiceRecord_DSound::Idle() +{ + UpdateWrapping(); +} + +int VoiceRecord_DSound::GetRecordedData( short *pOut, int nSamples ) +{ + if(!m_pCaptureBuffer) + { + assert(false); + return 0; + } + + DWORD dwStatus; + HRESULT hr = m_pCaptureBuffer->GetStatus(&dwStatus); + if(FAILED(hr) || !(dwStatus & DSCBSTATUS_CAPTURING)) + return 0; + + Idle(); // Update wrapping.. + + DWORD nBytesWanted = (DWORD)( nSamples << 1 ); + + DWORD dwReadPos; + hr = m_pCaptureBuffer->GetCurrentPosition( NULL, &dwReadPos); + if(FAILED(hr)) + return 0; + + dwReadPos += m_WrapOffset; + + // Read the range (dwReadPos-nSamplesWanted, dwReadPos), but don't re-read data we've already read. + DWORD readStart = Max( dwReadPos - nBytesWanted, (DWORD)0u ); + if ( readStart < m_LastReadPos ) + { + readStart = m_LastReadPos; + } + + // Lock the buffer. + LPVOID pData[2]; + DWORD dataLen[2]; + + hr = m_pCaptureBuffer->Lock( + readStart % NumCaptureBufferBytes(), // Offset. + dwReadPos - readStart, // Number of bytes to lock. + &pData[0], // Buffer 1. + &dataLen[0], // Buffer 1 length. + &pData[1], // Buffer 2. + &dataLen[1], // Buffer 2 length. + 0 // Flags. + ); + + if(FAILED(hr)) + return 0; + + // Hopefully we didn't get too much data back! + if((dataLen[0]+dataLen[1]) > nBytesWanted ) + { + assert(false); + m_pCaptureBuffer->Unlock(pData[0], dataLen[0], pData[1], dataLen[1]); + return 0; + } + + // Copy the data to the output. + memcpy(pOut, pData[0], dataLen[0]); + memcpy(&pOut[dataLen[0]/2], pData[1], dataLen[1]); + + m_pCaptureBuffer->Unlock(pData[0], dataLen[0], pData[1], dataLen[1]); + + // Last Read Position + m_LastReadPos = dwReadPos; + // Return sample count (not bytes) + return (dataLen[0] + dataLen[1]) >> 1; +} + + +void VoiceRecord_DSound::UpdateWrapping() +{ + if(!m_pCaptureBuffer) + return; + + // Has the buffer wrapped? + if ( VCRHook_WaitForSingleObject(m_hWrapEvent, 0) == WAIT_OBJECT_0 ) + { + m_WrapOffset += m_nCaptureBufferBytes; + } +} + + + +IVoiceRecord* CreateVoiceRecord_DSound(int sampleRate) +{ + VoiceRecord_DSound *pRecord = new VoiceRecord_DSound; + if(pRecord && pRecord->Init(sampleRate)) + { + return pRecord; + } + else + { + if(pRecord) + pRecord->Release(); + + return NULL; + } +} + diff --git a/engine/audio/private/voice_record_mac_audioqueue.cpp b/engine/audio/private/voice_record_mac_audioqueue.cpp new file mode 100644 index 0000000..e26ba4e --- /dev/null +++ b/engine/audio/private/voice_record_mac_audioqueue.cpp @@ -0,0 +1,528 @@ +//========= Copyright 1996-2009, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// This module implements the voice record and compression functions + +#include <Carbon/Carbon.h> +#include <AudioUnit/AudioUnit.h> +#include <AudioToolbox/AudioToolbox.h> + +#include "tier0/platform.h" +#include "tier0/threadtools.h" +//#include "tier0/vcrmode.h" +#include "ivoicerecord.h" + + +#define kNumSecAudioBuffer 1.0f + +// ------------------------------------------------------------------------------ +// VoiceRecord_AudioQueue +// ------------------------------------------------------------------------------ + +class VoiceRecord_AudioQueue : public IVoiceRecord +{ +public: + + VoiceRecord_AudioQueue(); + virtual ~VoiceRecord_AudioQueue(); + + // IVoiceRecord. + virtual void Release(); + + virtual bool RecordStart(); + virtual void RecordStop(); + + // Initialize. The format of the data we expect from the provider is + // 8-bit signed mono at the specified sample rate. + virtual bool Init( int nSampleRate ); + + virtual void Idle(); + + // Get the most recent N samples. + virtual int GetRecordedData(short *pOut, int nSamplesWanted ); + + AudioUnit GetAudioUnit() { return m_AudioUnit; } + AudioConverterRef GetConverter() { return m_Converter; } + void RenderBuffer( const short *pszBuf, int nSamples ); + bool BRecording() { return m_bRecordingAudio; } + void ClearThreadHandle() { m_hThread = NULL; m_bFirstInit = false; } + + AudioBufferList m_MicInputBuffer; + AudioBufferList m_ConverterBuffer; + void *m_pMicInputBuffer; + + int m_nMicInputSamplesAvaialble; + float m_flSampleRateConversion; + int m_nBufferFrameSize; + int m_ConverterBufferSize; + int m_MicInputBufferSize; + int m_InputBytesPerPacket; + +private: + bool InitalizeInterfaces(); // Initialize the openal capture buffers and other interfaces + void ReleaseInterfaces(); // Release openal buffers and other interfaces + void ClearInterfaces(); // Clear members. + + +private: + AudioUnit m_AudioUnit; + char *m_SampleBuffer; + int m_SampleBufferSize; + int m_nSampleRate; + bool m_bRecordingAudio; + bool m_bFirstInit; + ThreadHandle_t m_hThread; + AudioConverterRef m_Converter; + + CInterlockedUInt m_SampleBufferReadPos; + CInterlockedUInt m_SampleBufferWritePos; + + //UInt32 nPackets = 0; + //bool bHaveListData = false; + + +}; + + +VoiceRecord_AudioQueue::VoiceRecord_AudioQueue() : +m_nSampleRate( 0 ), m_AudioUnit( NULL ), m_SampleBufferSize(0), m_SampleBuffer(NULL), +m_SampleBufferReadPos(0), m_SampleBufferWritePos(0), m_bRecordingAudio(false), m_hThread( NULL ), m_bFirstInit( true ) +{ + ClearInterfaces(); +} + + +VoiceRecord_AudioQueue::~VoiceRecord_AudioQueue() +{ + ReleaseInterfaces(); + if ( m_hThread ) + ReleaseThreadHandle( m_hThread ); + m_hThread = NULL; +} + + +void VoiceRecord_AudioQueue::Release() +{ + ReleaseInterfaces(); +} + +uintp StartAudio( void *pRecorder ) +{ + VoiceRecord_AudioQueue *vr = (VoiceRecord_AudioQueue *)pRecorder; + if ( vr ) + { + //printf( "AudioOutputUnitStart\n" ); + AudioOutputUnitStart( vr->GetAudioUnit() ); + vr->ClearThreadHandle(); + } + //printf( "StartAudio thread done\n" ); + + return 0; +} + +bool VoiceRecord_AudioQueue::RecordStart() +{ + if ( !m_AudioUnit ) + return false; + + if ( m_bFirstInit ) + m_hThread = CreateSimpleThread( StartAudio, this ); + else + AudioOutputUnitStart( m_AudioUnit ); + + m_SampleBufferReadPos = m_SampleBufferWritePos = 0; + + m_bRecordingAudio = true; + //printf( "VoiceRecord_AudioQueue::RecordStart\n" ); + return ( !m_bFirstInit || m_hThread != NULL ); +} + + +void VoiceRecord_AudioQueue::RecordStop() +{ + // Stop capturing. + if ( m_AudioUnit && m_bRecordingAudio ) + { + AudioOutputUnitStop( m_AudioUnit ); + //printf( "AudioOutputUnitStop\n" ); + } + + m_SampleBufferReadPos = m_SampleBufferWritePos = 0; + m_bRecordingAudio = false; + + if ( m_hThread ) + ReleaseThreadHandle( m_hThread ); + m_hThread = NULL; +} + + + +OSStatus ComplexBufferFillPlayback( AudioConverterRef inAudioConverter, + UInt32 *ioNumberDataPackets, + AudioBufferList *ioData, + AudioStreamPacketDescription **outDataPacketDesc, + void *inUserData) +{ + VoiceRecord_AudioQueue *vr = (VoiceRecord_AudioQueue *)inUserData; + if ( !vr->BRecording() ) + return noErr; + + if ( vr->m_nMicInputSamplesAvaialble ) + { + int nBytesRequired = *ioNumberDataPackets * vr->m_InputBytesPerPacket; + int nBytesAvailable = vr->m_nMicInputSamplesAvaialble*vr->m_InputBytesPerPacket; + + if ( nBytesRequired < nBytesAvailable ) + { + ioData->mBuffers[0].mData = vr->m_MicInputBuffer.mBuffers[0].mData; + ioData->mBuffers[0].mDataByteSize = nBytesRequired; + vr->m_MicInputBuffer.mBuffers[0].mData = (char *)vr->m_MicInputBuffer.mBuffers[0].mData+nBytesRequired; + vr->m_MicInputBuffer.mBuffers[0].mDataByteSize = nBytesAvailable - nBytesRequired; + } + else + { + ioData->mBuffers[0].mData = vr->m_MicInputBuffer.mBuffers[0].mData; + ioData->mBuffers[0].mDataByteSize = nBytesAvailable; + vr->m_MicInputBuffer.mBuffers[0].mData = vr->m_pMicInputBuffer; + vr->m_MicInputBuffer.mBuffers[0].mDataByteSize = vr->m_MicInputBufferSize; + } + + *ioNumberDataPackets = ioData->mBuffers[0].mDataByteSize / vr->m_InputBytesPerPacket; + vr->m_nMicInputSamplesAvaialble = nBytesAvailable / vr->m_InputBytesPerPacket - *ioNumberDataPackets; + } + else + { + *ioNumberDataPackets = 0; + return -1; + } + + return noErr; +} + + + + +static OSStatus recordingCallback(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, + UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) +{ + VoiceRecord_AudioQueue *vr = (VoiceRecord_AudioQueue *)inRefCon; + if ( !vr->BRecording() ) + return noErr; + + OSStatus err = noErr; + if ( vr->m_nMicInputSamplesAvaialble == 0 ) + { + err = AudioUnitRender( vr->GetAudioUnit(), ioActionFlags, inTimeStamp, 1, inNumberFrames, &vr->m_MicInputBuffer ); + if ( err == noErr ) + vr->m_nMicInputSamplesAvaialble = vr->m_MicInputBuffer.mBuffers[0].mDataByteSize / vr->m_InputBytesPerPacket; + } + + if ( vr->m_nMicInputSamplesAvaialble > 0 ) + { + UInt32 nConverterSamples = ceil(vr->m_nMicInputSamplesAvaialble/vr->m_flSampleRateConversion); + vr->m_ConverterBuffer.mBuffers[0].mDataByteSize = vr->m_ConverterBufferSize; + OSStatus err = AudioConverterFillComplexBuffer( vr->GetConverter(), + ComplexBufferFillPlayback, + vr, + &nConverterSamples, + &vr->m_ConverterBuffer, + NULL ); + if ( err == noErr || err == -1 ) + vr->RenderBuffer( (short *)vr->m_ConverterBuffer.mBuffers[0].mData, vr->m_ConverterBuffer.mBuffers[0].mDataByteSize/sizeof(short) ); + } + + return err; +} + + +void VoiceRecord_AudioQueue::RenderBuffer( const short *pszBuf, int nSamples ) +{ + int samplePos = m_SampleBufferWritePos; + int samplePosBefore = samplePos; + int readPos = m_SampleBufferReadPos; + bool bBeforeRead = false; + if ( samplePos < readPos ) + bBeforeRead = true; + char *pOut = (char *)(m_SampleBuffer + samplePos); + int nFirstCopy = MIN( nSamples*sizeof(short), m_SampleBufferSize - samplePos ); + memcpy( pOut, pszBuf, nFirstCopy ); + samplePos += nFirstCopy; + if ( nSamples*sizeof(short) > nFirstCopy ) + { + nSamples -= ( nFirstCopy / sizeof(short) ); + samplePos = 0; + memcpy( m_SampleBuffer, pszBuf + nFirstCopy, nSamples * sizeof(short) ); + samplePos += nSamples * sizeof(short); + } + + m_SampleBufferWritePos = samplePos%m_SampleBufferSize; + if ( (bBeforeRead && samplePos > readPos) ) + { + m_SampleBufferReadPos = (readPos+m_SampleBufferSize/2)%m_SampleBufferSize; // if we crossed the read pointer then bump it forward + //printf( "Crossed %d %d (%d)\n", (int)samplePosBefore, (int)samplePos, readPos ); + } +} + + +bool VoiceRecord_AudioQueue::InitalizeInterfaces() +{ + //printf( "Initializing audio queue recorder\n" ); + // Describe audio component + ComponentDescription desc; + desc.componentType = kAudioUnitType_Output; + desc.componentSubType = kAudioUnitSubType_HALOutput; + desc.componentManufacturer = kAudioUnitManufacturer_Apple; + desc.componentFlags = 0; + desc.componentFlagsMask = 0; + + Component comp = FindNextComponent(NULL, &desc); + if (comp == NULL) + return false; + + OSStatus status = OpenAComponent(comp, &m_AudioUnit); + if ( status != noErr ) + return false; + + // Enable IO for recording + UInt32 flag = 1; + status = AudioUnitSetProperty( m_AudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, + 1, &flag, sizeof(flag)); + if ( status != noErr ) + return false; + + // disable output on the device + flag = 0; + status = AudioUnitSetProperty( m_AudioUnit,kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, + 0, &flag,sizeof(flag)); + if ( status != noErr ) + return false; + + UInt32 size = sizeof(AudioDeviceID); + AudioDeviceID inputDevice; + status = AudioHardwareGetProperty(kAudioHardwarePropertyDefaultInputDevice,&size, &inputDevice); + if ( status != noErr ) + return false; + + status =AudioUnitSetProperty( m_AudioUnit, kAudioOutputUnitProperty_CurrentDevice, kAudioUnitScope_Global, + 0, &inputDevice, sizeof(inputDevice)); + if ( status != noErr ) + return false; + + // Describe format + AudioStreamBasicDescription audioDeviceFormat; + size = sizeof(AudioStreamBasicDescription); + status = AudioUnitGetProperty( m_AudioUnit, + kAudioUnitProperty_StreamFormat, + kAudioUnitScope_Input, + 1, // input bus + &audioDeviceFormat, + &size); + + if ( status != noErr ) + return false; + + // we only want mono audio, so if they have a stero input ask for mono + if ( audioDeviceFormat.mChannelsPerFrame == 2 ) + { + audioDeviceFormat.mChannelsPerFrame = 1; + audioDeviceFormat.mBytesPerPacket /= 2; + audioDeviceFormat.mBytesPerFrame /= 2; + } + + // Apply format + status = AudioUnitSetProperty( m_AudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, + 1, &audioDeviceFormat, sizeof(audioDeviceFormat) ); + if ( status != noErr ) + return false; + + AudioStreamBasicDescription audioOutputFormat; + audioOutputFormat = audioDeviceFormat; + audioOutputFormat.mFormatID = kAudioFormatLinearPCM; + audioOutputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; + audioOutputFormat.mBytesPerPacket = 2; // 16-bit samples * 1 channels + audioOutputFormat.mFramesPerPacket = 1; + audioOutputFormat.mBytesPerFrame = 2; // 16-bit samples * 1 channels + audioOutputFormat.mChannelsPerFrame = 1; + audioOutputFormat.mBitsPerChannel = 16; + audioOutputFormat.mReserved = 0; + + audioOutputFormat.mSampleRate = m_nSampleRate; + + m_flSampleRateConversion = audioDeviceFormat.mSampleRate / audioOutputFormat.mSampleRate; + + // setup sample rate conversion + status = AudioConverterNew( &audioDeviceFormat, &audioOutputFormat, &m_Converter ); + if ( status != noErr ) + return false; + + + UInt32 primeMethod = kConverterPrimeMethod_None; + status = AudioConverterSetProperty( m_Converter, kAudioConverterPrimeMethod, sizeof(UInt32), &primeMethod); + if ( status != noErr ) + return false; + + UInt32 quality = kAudioConverterQuality_Medium; + status = AudioConverterSetProperty( m_Converter, kAudioConverterSampleRateConverterQuality, sizeof(UInt32), &quality); + if ( status != noErr ) + return false; + + // Set input callback + AURenderCallbackStruct callbackStruct; + callbackStruct.inputProc = recordingCallback; + callbackStruct.inputProcRefCon = this; + status = AudioUnitSetProperty( m_AudioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, + 0, &callbackStruct, sizeof(callbackStruct) ); + if ( status != noErr ) + return false; + + UInt32 bufferFrameSize; + size = sizeof(bufferFrameSize); + status = AudioDeviceGetProperty( inputDevice, 1, 1, kAudioDevicePropertyBufferFrameSize, &size, &bufferFrameSize ); + if ( status != noErr ) + return false; + + m_nBufferFrameSize = bufferFrameSize; + + // allocate the input and conversion sound storage buffers + m_MicInputBuffer.mNumberBuffers = 1; + m_MicInputBuffer.mBuffers[0].mDataByteSize = m_nBufferFrameSize*audioDeviceFormat.mBitsPerChannel/8*audioDeviceFormat.mChannelsPerFrame; + m_MicInputBuffer.mBuffers[0].mData = malloc( m_MicInputBuffer.mBuffers[0].mDataByteSize ); + m_MicInputBuffer.mBuffers[0].mNumberChannels = audioDeviceFormat.mChannelsPerFrame; + m_pMicInputBuffer = m_MicInputBuffer.mBuffers[0].mData; + m_MicInputBufferSize = m_MicInputBuffer.mBuffers[0].mDataByteSize; + + m_InputBytesPerPacket = audioDeviceFormat.mBytesPerPacket; + + m_ConverterBuffer.mNumberBuffers = 1; + m_ConverterBuffer.mBuffers[0].mDataByteSize = m_nBufferFrameSize*audioOutputFormat.mBitsPerChannel/8*audioOutputFormat.mChannelsPerFrame; + m_ConverterBuffer.mBuffers[0].mData = malloc( m_MicInputBuffer.mBuffers[0].mDataByteSize ); + m_ConverterBuffer.mBuffers[0].mNumberChannels = 1; + + m_ConverterBufferSize = m_ConverterBuffer.mBuffers[0].mDataByteSize; + + m_nMicInputSamplesAvaialble = 0; + + + m_SampleBufferReadPos = m_SampleBufferWritePos = 0; + m_SampleBufferSize = ceil( kNumSecAudioBuffer * m_nSampleRate * audioOutputFormat.mBytesPerPacket ); + m_SampleBuffer = (char *)malloc( m_SampleBufferSize ); + memset( m_SampleBuffer, 0x0, m_SampleBufferSize ); + + DevMsg( "Initialized AudioQueue record interface\n" ); + return true; +} + +bool VoiceRecord_AudioQueue::Init( int nSampleRate ) +{ + if ( m_AudioUnit && m_nSampleRate != nSampleRate ) + { + // Need to recreate interfaces with different sample rate + ReleaseInterfaces(); + ClearInterfaces(); + } + m_nSampleRate = nSampleRate; + + // Re-initialize the capture buffer if neccesary + if ( !m_AudioUnit ) + { + InitalizeInterfaces(); + } + + m_SampleBufferReadPos = m_SampleBufferWritePos = 0; + + //printf( "VoiceRecord_AudioQueue::Init()\n" ); + // Initialise + OSStatus status = AudioUnitInitialize( m_AudioUnit ); + if ( status != noErr ) + return false; + + return true; +} + + +void VoiceRecord_AudioQueue::ReleaseInterfaces() +{ + AudioOutputUnitStop( m_AudioUnit ); + AudioConverterDispose( m_Converter ); + AudioUnitUninitialize( m_AudioUnit ); + m_AudioUnit = NULL; + m_Converter = NULL; +} + + +void VoiceRecord_AudioQueue::ClearInterfaces() +{ + m_AudioUnit = NULL; + m_Converter = NULL; + m_SampleBufferReadPos = m_SampleBufferWritePos = 0; + if ( m_SampleBuffer ) + free( m_SampleBuffer ); + m_SampleBuffer = NULL; + + if ( m_MicInputBuffer.mBuffers[0].mData ) + free( m_MicInputBuffer.mBuffers[0].mData ); + if ( m_ConverterBuffer.mBuffers[0].mData ) + free( m_ConverterBuffer.mBuffers[0].mData ); + m_MicInputBuffer.mBuffers[0].mData = NULL; + m_ConverterBuffer.mBuffers[0].mData = NULL; +} + + +void VoiceRecord_AudioQueue::Idle() +{ +} + + +int VoiceRecord_AudioQueue::GetRecordedData(short *pOut, int nSamples ) +{ + if ( !m_SampleBuffer ) + return 0; + + int cbSamples = nSamples*2; // convert to bytes + int writePos = m_SampleBufferWritePos; + int readPos = m_SampleBufferReadPos; + + int nOutstandingSamples = ( writePos - readPos ); + if ( readPos > writePos ) // writing has wrapped around + { + nOutstandingSamples = writePos + ( m_SampleBufferSize - readPos ); + } + + if ( !nOutstandingSamples ) + return 0; + + if ( nOutstandingSamples < cbSamples ) + cbSamples = nOutstandingSamples; // clamp to the number of samples we have available + + memcpy( (char *)pOut, m_SampleBuffer + readPos, MIN( cbSamples, m_SampleBufferSize - readPos ) ); + if ( cbSamples > ( m_SampleBufferSize - readPos ) ) + { + int offset = m_SampleBufferSize - readPos; + cbSamples -= offset; + readPos = 0; + memcpy( (char *)pOut + offset, m_SampleBuffer, cbSamples ); + } + readPos+=cbSamples; + m_SampleBufferReadPos = readPos%m_SampleBufferSize; + //printf( "Returning %d samples, %d %d (%d)\n", cbSamples/2, (int)m_SampleBufferReadPos, (int)m_SampleBufferWritePos, m_SampleBufferSize ); + return cbSamples/2; +} + + +VoiceRecord_AudioQueue g_AudioQueueVoiceRecord; +IVoiceRecord* CreateVoiceRecord_AudioQueue( int sampleRate ) +{ + if ( g_AudioQueueVoiceRecord.Init( sampleRate ) ) + { + return &g_AudioQueueVoiceRecord; + } + else + { + g_AudioQueueVoiceRecord.Release(); + return NULL; + } +} diff --git a/engine/audio/private/voice_record_openal.cpp b/engine/audio/private/voice_record_openal.cpp new file mode 100644 index 0000000..679bf7d --- /dev/null +++ b/engine/audio/private/voice_record_openal.cpp @@ -0,0 +1,209 @@ +//========= Copyright 1996-2009, Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +// +//=============================================================================// +// This module implements the voice record and compression functions + +//#include "audio_pch.h" +//#include "voice.h" +#include "tier0/platform.h" +#include "ivoicerecord.h" + +#include <assert.h> + +#ifndef POSIX +class VoiceRecord_OpenAL : public IVoiceRecord +{ +public: + VoiceRecord_OpenAL() {} + virtual ~VoiceRecord_OpenAL() {} + virtual void Release() {} + virtual bool RecordStart() { return true; } + virtual void RecordStop() {} + virtual bool Init(int sampleRate) { return true; } + virtual void Idle() {} + virtual int GetRecordedData(short *pOut, int nSamplesWanted ) { return 0; } +}; +IVoiceRecord* CreateVoiceRecord_DSound(int sampleRate) { return new VoiceRecord_OpenAL; } + +#else + +#define min(a,b) (((a) < (b)) ? (a) : (b)) +#ifdef OSX +#include <Carbon/Carbon.h> +#include <OpenAL/al.h> +#else +#include <AL/al.h> +#endif +#include "openal/alc.h" + +// ------------------------------------------------------------------------------ +// VoiceRecord_OpenAL +// ------------------------------------------------------------------------------ + +class VoiceRecord_OpenAL : public IVoiceRecord +{ +protected: + + virtual ~VoiceRecord_OpenAL(); + + + // IVoiceRecord. +public: + + VoiceRecord_OpenAL(); + virtual void Release(); + + virtual bool RecordStart(); + virtual void RecordStop(); + + // Initialize. The format of the data we expect from the provider is + // 8-bit signed mono at the specified sample rate. + virtual bool Init(int sampleRate); + + virtual void Idle(); + + // Get the most recent N samples. + virtual int GetRecordedData(short *pOut, int nSamplesWanted ); + +private: + bool InitalizeInterfaces(); // Initialize the openal capture buffers and other interfaces + void ReleaseInterfaces(); // Release openal buffers and other interfaces + void ClearInterfaces(); // Clear members. + + +private: + ALCdevice *m_Device; + + int m_nSampleRate; +}; + + + +VoiceRecord_OpenAL::VoiceRecord_OpenAL() : +m_nSampleRate( 0 ), m_Device( NULL ) +{ + ClearInterfaces(); +} + +VoiceRecord_OpenAL::~VoiceRecord_OpenAL() +{ + ReleaseInterfaces(); +} + + +void VoiceRecord_OpenAL::Release() +{ + delete this; +} + +bool VoiceRecord_OpenAL::RecordStart() +{ + + // Re-initialize the capture buffer if neccesary (should always be) + if ( !m_Device ) + { + InitalizeInterfaces(); + } + + if ( !m_Device ) + return false; + + alcGetError(m_Device); + + alcCaptureStart(m_Device); + + const ALenum error = alcGetError(m_Device); + return error == AL_NO_ERROR; +} + + +void VoiceRecord_OpenAL::RecordStop() +{ + // Stop capturing. + if ( m_Device ) + { + alcCaptureStop( m_Device ); + } + + // Release the capture buffer interface and any other resources that are no + // longer needed + ReleaseInterfaces(); +} + +bool VoiceRecord_OpenAL::InitalizeInterfaces() +{ + m_Device = alcCaptureOpenDevice( NULL, m_nSampleRate, AL_FORMAT_MONO16, m_nSampleRate * 10 * 2); + const ALenum error = alcGetError(m_Device); + const bool result = error == AL_NO_ERROR; + return m_Device != NULL && result; +} + +bool VoiceRecord_OpenAL::Init(int sampleRate) +{ + m_nSampleRate = sampleRate; + + ReleaseInterfaces(); + + return true; +} + + +void VoiceRecord_OpenAL::ReleaseInterfaces() +{ + alcCaptureCloseDevice(m_Device); + ClearInterfaces(); +} + + +void VoiceRecord_OpenAL::ClearInterfaces() +{ + m_Device = NULL; +} + + +void VoiceRecord_OpenAL::Idle() +{ +} + + +int VoiceRecord_OpenAL::GetRecordedData(short *pOut, int nSamples ) +{ + int frameCount = 0; + alcGetIntegerv( m_Device,ALC_CAPTURE_SAMPLES,1,&frameCount ); + if ( frameCount > 0 ) + { + frameCount = min( nSamples, frameCount ); + alcCaptureSamples( m_Device, pOut, frameCount ); + if ( alcGetError(m_Device) != ALC_NO_ERROR ) + { + return 0; + } + return frameCount; + } + return 0; +} + + + +IVoiceRecord* CreateVoiceRecord_OpenAL(int sampleRate) +{ + VoiceRecord_OpenAL *pRecord = new VoiceRecord_OpenAL; + if ( pRecord && pRecord->Init(sampleRate) ) + { + return pRecord; + } + else + { + if ( pRecord ) + { + pRecord->Release(); + } + + return NULL; + } +} +#endif diff --git a/engine/audio/private/voice_sound_engine_interface.cpp b/engine/audio/private/voice_sound_engine_interface.cpp new file mode 100644 index 0000000..3ee5e59 --- /dev/null +++ b/engine/audio/private/voice_sound_engine_interface.cpp @@ -0,0 +1,377 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#include "audio_pch.h" +#include <assert.h> +#include "voice.h" +#include "ivoicecodec.h" + +#if defined( _X360 ) +#include "xauddefs.h" +#endif + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// ------------------------------------------------------------------------- // +// CAudioSourceVoice. +// This feeds the data from an incoming voice channel (a guy on the server +// who is speaking) into the sound engine. +// ------------------------------------------------------------------------- // + +class CAudioSourceVoice : public CAudioSourceWave +{ +public: + CAudioSourceVoice(CSfxTable *pSfx, int iEntity); + virtual ~CAudioSourceVoice(); + + virtual int GetType( void ) + { + return AUDIO_SOURCE_VOICE; + } + virtual void GetCacheData( CAudioSourceCachedInfo *info ) + { + Assert( 0 ); + } + + + virtual CAudioMixer *CreateMixer( int initialStreamPosition = 0 ); + virtual int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ); + virtual int SampleRate( void ); + + // Sample size is in bytes. It will not be accurate for compressed audio. This is a best estimate. + // The compressed audio mixers understand this, but in general do not assume that SampleSize() * SampleCount() = filesize + // or even that SampleSize() is 100% accurate due to compression. + virtual int SampleSize( void ); + + // Total number of samples in this source. NOTE: Some sources are infinite (mic input), they should return + // a count equal to one second of audio at their current rate. + virtual int SampleCount( void ); + + virtual bool IsVoiceSource() {return true;} + + virtual bool IsLooped() {return false;} + virtual bool IsStreaming() {return true;} + virtual bool IsStereoWav() {return false;} + virtual int GetCacheStatus() {return AUDIO_IS_LOADED;} + virtual void CacheLoad() {} + virtual void CacheUnload() {} + virtual CSentence *GetSentence() {return NULL;} + + virtual int ZeroCrossingBefore( int sample ) {return sample;} + virtual int ZeroCrossingAfter( int sample ) {return sample;} + + // mixer's references + virtual void ReferenceAdd( CAudioMixer *pMixer ); + virtual void ReferenceRemove( CAudioMixer *pMixer ); + + // check reference count, return true if nothing is referencing this + virtual bool CanDelete(); + + virtual void Prefetch() {} + + // Nothing, not a cache object... + virtual void CheckAudioSourceCache() {} + +private: + + class CWaveDataVoice : public IWaveData + { + public: + CWaveDataVoice( CAudioSourceWave &source ) : m_source(source) {} + ~CWaveDataVoice( void ) {} + + virtual CAudioSource &Source( void ) + { + return m_source; + } + + // this file is in memory, simply pass along the data request to the source + virtual int ReadSourceData( void **pData, int sampleIndex, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) + { + return m_source.GetOutputData( pData, sampleIndex, sampleCount, copyBuf ); + } + + virtual bool IsReadyToMix() + { + return true; + } + + private: + CAudioSourceWave &m_source; // pointer to source + }; + + +private: + CAudioSourceVoice( const CAudioSourceVoice & ); + + // Which entity's voice this is for. + int m_iChannel; + + // How many mixers are referencing us. + int m_refCount; +}; + + + +// ----------------------------------------------------------------------------- // +// Globals. +// ----------------------------------------------------------------------------- // + +// The format we sample voice in. +extern WAVEFORMATEX g_VoiceSampleFormat; + +class CVoiceSfx : public CSfxTable +{ +public: + virtual const char *getname() + { + return "?VoiceSfx"; + } +}; + +static CVoiceSfx g_CVoiceSfx[VOICE_NUM_CHANNELS]; + +static float g_VoiceOverdriveDuration = 0; +static bool g_bVoiceOverdriveOn = false; + +// When voice is on, all other sounds are decreased by this factor. +static ConVar voice_overdrive( "voice_overdrive", "2" ); +static ConVar voice_overdrivefadetime( "voice_overdrivefadetime", "0.4" ); // How long it takes to fade in and out of the voice overdrive. + +// The sound engine uses this to lower all sound volumes. +// All non-voice sounds are multiplied by this and divided by 256. +int g_SND_VoiceOverdriveInt = 256; + + +extern int Voice_SamplesPerSec(); +extern int Voice_AvgBytesPerSec(); + +// ----------------------------------------------------------------------------- // +// CAudioSourceVoice implementation. +// ----------------------------------------------------------------------------- // + +CAudioSourceVoice::CAudioSourceVoice( CSfxTable *pSfx, int iChannel ) + : CAudioSourceWave( pSfx ) +{ + m_iChannel = iChannel; + m_refCount = 0; + + WAVEFORMATEX tmp = g_VoiceSampleFormat; + tmp.nSamplesPerSec = Voice_SamplesPerSec(); + tmp.nAvgBytesPerSec = Voice_AvgBytesPerSec(); + Init((char*)&tmp, sizeof(tmp)); + m_sampleCount = tmp.nSamplesPerSec; +} + +CAudioSourceVoice::~CAudioSourceVoice() +{ + Voice_OnAudioSourceShutdown( m_iChannel ); +} + +CAudioMixer *CAudioSourceVoice::CreateMixer( int initialStreamPosition ) +{ + CWaveDataVoice *pVoice = new CWaveDataVoice(*this); + if(!pVoice) + return NULL; + + CAudioMixer *pMixer = CreateWaveMixer( pVoice, WAVE_FORMAT_PCM, 1, BYTES_PER_SAMPLE*8, 0 ); + if(!pMixer) + { + delete pVoice; + return NULL; + } + + return pMixer; +} + +int CAudioSourceVoice::GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) +{ + int nSamplesGotten = Voice_GetOutputData( + m_iChannel, + copyBuf, + AUDIOSOURCE_COPYBUF_SIZE, + samplePosition, + sampleCount ); + + // If there weren't enough bytes in the received data channel, pad it with zeros. + if( nSamplesGotten < sampleCount ) + { + memset( ©Buf[nSamplesGotten], 0, (sampleCount - nSamplesGotten) * BYTES_PER_SAMPLE ); + nSamplesGotten = sampleCount; + } + + *pData = copyBuf; + return nSamplesGotten; +} + +int CAudioSourceVoice::SampleRate() +{ + return Voice_SamplesPerSec(); +} + +int CAudioSourceVoice::SampleSize() +{ + return BYTES_PER_SAMPLE; +} + +int CAudioSourceVoice::SampleCount() +{ + return Voice_SamplesPerSec(); +} + +void CAudioSourceVoice::ReferenceAdd(CAudioMixer *pMixer) +{ + m_refCount++; +} + +void CAudioSourceVoice::ReferenceRemove(CAudioMixer *pMixer) +{ + m_refCount--; + if ( m_refCount <= 0 ) + delete this; +} + +bool CAudioSourceVoice::CanDelete() +{ + return m_refCount == 0; +} + + +// ----------------------------------------------------------------------------- // +// Interface implementation. +// ----------------------------------------------------------------------------- // + +bool VoiceSE_Init() +{ + if( !snd_initialized ) + return false; + + g_SND_VoiceOverdriveInt = 256; + return true; +} + +void VoiceSE_Term() +{ + // Disable voice ducking. + g_SND_VoiceOverdriveInt = 256; +} + + +void VoiceSE_Idle(float frametime) +{ + g_SND_VoiceOverdriveInt = 256; + + if( g_bVoiceOverdriveOn ) + { + g_VoiceOverdriveDuration = min( g_VoiceOverdriveDuration+frametime, voice_overdrivefadetime.GetFloat() ); + } + else + { + if(g_VoiceOverdriveDuration == 0) + return; + + g_VoiceOverdriveDuration = max(g_VoiceOverdriveDuration-frametime, 0.f); + } + + float percent = g_VoiceOverdriveDuration / voice_overdrivefadetime.GetFloat(); + percent = (float)(-cos(percent * 3.1415926535) * 0.5 + 0.5); // Smooth it out.. + float voiceOverdrive = 1 + (voice_overdrive.GetFloat() - 1) * percent; + g_SND_VoiceOverdriveInt = (int)(256 / voiceOverdrive); +} + + +int VoiceSE_StartChannel( + int iChannel, //! Which channel to start. + int iEntity, + bool bProximity, + int nViewEntityIndex ) +{ + Assert( iChannel >= 0 && iChannel < VOICE_NUM_CHANNELS ); + + // Start the sound. + CSfxTable *sfx = &g_CVoiceSfx[iChannel]; + sfx->pSource = NULL; + Vector vOrigin(0,0,0); + + StartSoundParams_t params; + params.staticsound = false; + params.entchannel = (CHAN_VOICE_BASE+iChannel); + params.pSfx = sfx; + params.origin = vOrigin; + params.fvol = 1.0f; + params.flags = 0; + params.pitch = PITCH_NORM; + + + if ( bProximity == true ) + { + params.bUpdatePositions = true; + params.soundlevel = SNDLVL_TALKING; + params.soundsource = iEntity; + } + else + { + params.soundlevel = SNDLVL_IDLE; + params.soundsource = nViewEntityIndex; + } + + + return S_StartSound( params ); +} + +void VoiceSE_EndChannel( + int iChannel, //! Which channel to stop. + int iEntity + ) +{ + Assert( iChannel >= 0 && iChannel < VOICE_NUM_CHANNELS ); + + S_StopSound( iEntity, CHAN_VOICE_BASE+iChannel ); + + // Start the sound. + CSfxTable *sfx = &g_CVoiceSfx[iChannel]; + sfx->pSource = NULL; +} + +void VoiceSE_StartOverdrive() +{ + g_bVoiceOverdriveOn = true; +} + +void VoiceSE_EndOverdrive() +{ + g_bVoiceOverdriveOn = false; +} + + +void VoiceSE_InitMouth(int entnum) +{ +} + +void VoiceSE_CloseMouth(int entnum) +{ +} + +void VoiceSE_MoveMouth(int entnum, short *pSamples, int nSamples) +{ +} + + +CAudioSource* Voice_SetupAudioSource( int soundsource, int entchannel ) +{ + int iChannel = entchannel - CHAN_VOICE_BASE; + if( iChannel >= 0 && iChannel < VOICE_NUM_CHANNELS ) + { + CSfxTable *sfx = &g_CVoiceSfx[iChannel]; + return new CAudioSourceVoice( sfx, iChannel ); + } + else + return NULL; +} + + diff --git a/engine/audio/private/voice_sound_engine_interface.h b/engine/audio/private/voice_sound_engine_interface.h new file mode 100644 index 0000000..10fcb1a --- /dev/null +++ b/engine/audio/private/voice_sound_engine_interface.h @@ -0,0 +1,102 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOICE_SOUND_ENGINE_INTERFACE_H +#define VOICE_SOUND_ENGINE_INTERFACE_H +#pragma once + + +/*! @defgroup VoiceSoundEngineInterface VoiceSoundEngineInterface +Abstracts out the sound engine for the voice code. +GoldSrc and Src each have a different implementation of this. +@{ +*/ + + + +//! Max number of receiving voice channels. +#define VOICE_NUM_CHANNELS 5 + +// ----------------------------------------------------------------------------- // +// Functions for voice.cpp. +// ----------------------------------------------------------------------------- // + +//! Initialize the sound engine interface. +bool VoiceSE_Init(); + +//! Shutdown the sound engine interface. +void VoiceSE_Term(); + +//! Called each frame. +void VoiceSE_Idle(float frametime); + + +//! Start audio playback on the specified voice channel. +//! Voice_GetChannelAudio is called by the mixer for each active channel. +int VoiceSE_StartChannel( + //! Which channel to start. + int iChannel, + int iEntity, + bool bProximity, + int nViewEntityIndex + ); + +//! Stop audio playback on the specified voice channel. +void VoiceSE_EndChannel( + //! Which channel to stop. + int iChannel, + int iEntity + ); + +//! Starts the voice overdrive (lowers volume of all sounds other than voice). +void VoiceSE_StartOverdrive(); +void VoiceSE_EndOverdrive(); + +//! Control mouth movement for an entity. +void VoiceSE_InitMouth(int entnum); +void VoiceSE_CloseMouth(int entnum); +void VoiceSE_MoveMouth(int entnum, short *pSamples, int nSamples); + + +// ----------------------------------------------------------------------------- // +// Functions for voice.cpp to implement. +// ----------------------------------------------------------------------------- // + +//! This function is implemented in voice.cpp. Gives 16-bit signed mono samples to the mixer. +//! \return Number of samples actually gotten. +int Voice_GetOutputData( + //! The voice channel it wants samples from. + const int iChannel, + //! The buffer to copy the samples into. + char *copyBuf, + //! Maximum size of copyBuf. + const int copyBufSize, + //! Which sample to start at. + const int samplePosition, + //! How many samples to get. + const int sampleCount + ); + +// This is called when an audio source is deleted by the sound engine. The voice could +// should detach whatever it needs to in order to free up the specified channel. +void Voice_OnAudioSourceShutdown( int iChannel ); + + +// ----------------------------------------------------------------------------- // +// Functions for the sound engine. +// ----------------------------------------------------------------------------- // + +class CAudioSource; +CAudioSource* Voice_SetupAudioSource( int soundsource, int entchannel ); + + + +/*! @} End VoiceSoundEngineInterface group */ + + +#endif // VOICE_SOUND_ENGINE_INTERFACE_H diff --git a/engine/audio/private/voice_wavefile.cpp b/engine/audio/private/voice_wavefile.cpp new file mode 100644 index 0000000..f4e554c --- /dev/null +++ b/engine/audio/private/voice_wavefile.cpp @@ -0,0 +1,119 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#undef fopen +#include <stdio.h> +#include "voice_wavefile.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +static unsigned long ReadDWord(FILE * fp) +{ + unsigned long ret; + fread( &ret, 4, 1, fp ); + return ret; +} + +static unsigned short ReadWord(FILE * fp) +{ + unsigned short ret; + fread( &ret, 2, 1, fp ); + return ret; +} + +static void WriteDWord(FILE * fp, unsigned long val) +{ + fwrite( &val, 4, 1, fp ); +} + +static void WriteWord(FILE * fp, unsigned short val) +{ + fwrite( &val, 2, 1, fp ); +} + + + +bool ReadWaveFile( + const char *pFilename, + char *&pData, + int &nDataBytes, + int &wBitsPerSample, + int &nChannels, + int &nSamplesPerSec) +{ + FILE * fp = fopen(pFilename, "rb"); + if(!fp) + return false; + + fseek( fp, 22, SEEK_SET ); + + nChannels = ReadWord(fp); + nSamplesPerSec = ReadDWord(fp); + + fseek(fp, 34, SEEK_SET); + wBitsPerSample = ReadWord(fp); + + fseek(fp, 40, SEEK_SET); + nDataBytes = ReadDWord(fp); + ReadDWord(fp); + pData = new char[nDataBytes]; + if(!pData) + { + fclose(fp); + return false; + } + fread(pData, nDataBytes, 1, fp); + fclose( fp ); + return true; +} + +bool WriteWaveFile( + const char *pFilename, + const char *pData, + int nBytes, + int wBitsPerSample, + int nChannels, + int nSamplesPerSec) +{ + FILE * fp = fopen(pFilename, "wb"); + if(!fp) + return false; + + // Write the RIFF chunk. + fwrite("RIFF", 4, 1, fp); + WriteDWord(fp, 0); + fwrite("WAVE", 4, 1, fp); + + + // Write the FORMAT chunk. + fwrite("fmt ", 4, 1, fp); + + WriteDWord(fp, 0x10); + WriteWord(fp, 1); // WAVE_FORMAT_PCM + WriteWord(fp, (unsigned short)nChannels); + WriteDWord(fp, (unsigned long)nSamplesPerSec); + WriteDWord(fp, (unsigned long)((wBitsPerSample / 8) * nChannels * nSamplesPerSec)); + WriteWord(fp, (unsigned short)((wBitsPerSample / 8) * nChannels)); + WriteWord(fp, (unsigned short)wBitsPerSample); + + // Write the DATA chunk. + fwrite("data", 4, 1, fp); + WriteDWord(fp, (unsigned long)nBytes); + fwrite(pData, nBytes, 1, fp); + + + // Go back and write the length of the riff file. + unsigned long dwVal = ftell(fp) - 8; + fseek( fp, 4, SEEK_SET ); + WriteDWord(fp, dwVal); + + fclose(fp); + return true; +} + + diff --git a/engine/audio/private/voice_wavefile.h b/engine/audio/private/voice_wavefile.h new file mode 100644 index 0000000..08afe86 --- /dev/null +++ b/engine/audio/private/voice_wavefile.h @@ -0,0 +1,34 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOICE_WAVEFILE_H +#define VOICE_WAVEFILE_H +#pragma once + + +// Load in a wave file. This isn't very flexible and is only guaranteed to work with files +// saved with WriteWaveFile. +bool ReadWaveFile( + const char *pFilename, + char *&pData, + int &nDataBytes, + int &wBitsPerSample, + int &nChannels, + int &nSamplesPerSec); + + +// Write out a wave file. +bool WriteWaveFile( + const char *pFilename, + const char *pData, + int nBytes, + int wBitsPerSample, + int nChannels, + int nSamplesPerSec); + + +#endif // VOICE_WAVEFILE_H diff --git a/engine/audio/private/vox.cpp b/engine/audio/private/vox.cpp new file mode 100644 index 0000000..80a3a2b --- /dev/null +++ b/engine/audio/private/vox.cpp @@ -0,0 +1,2862 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Voice / Sentence streaming & parsing code +// +// $Workfile: $ +// $Date: $ +// $NoKeywords: $ +//=============================================================================// + +//=============================================================================== +// VOX. Algorithms to load and play spoken text sentences from a file: +// +// In ambient sounds or entity sounds, precache the +// name of the sentence instead of the wave name, ie: !C1A2S4 +// +// During sound system init, the 'sentences.txt' is read. +// This file has the format: +// +// C1A2S4 agrunt/vox/You will be exterminated, surrender NOW. +// C1A2s5 hgrunt/vox/Radio check, over. +// ... +// +// There must be at least one space between the sentence name and the sentence. +// Sentences may be separated by one or more lines +// There may be tabs or spaces preceding the sentence name +// The sentence must end in a /n or /r +// Lines beginning with // are ignored as comments +// +// Period or comma will insert a pause in the wave unless +// the period or comma is the last character in the string. +// +// If first 2 chars of a word are upper case, word volume increased by 25% +// +// If last char of a word is a number from 0 to 9 +// then word will be pitch-shifted up by 0 to 9, where 0 is a small shift +// and 9 is a very high pitch shift. +// +// We alloc heap space to contain this data, and track total +// sentences read. A pointer to each sentence is maintained in g_Sentences. +// +// When sound is played back in S_StartDynamicSound or s_startstaticsound, we detect the !name +// format and lookup the actual sentence in the sentences array +// +// To play, we parse each word in the sentence, chain the words, and play the sentence +// each word's data is loaded directy from disk and freed right after playback. +//=============================================================================== + +#include "audio_pch.h" +#include "vox_private.h" +#include "characterset.h" +#include "vstdlib/random.h" +#include "engine/IEngineSound.h" +#include "utlsymbol.h" +#include "utldict.h" +#include "../../MapReslistGenerator.h" + +// memdbgon must be the last include file in a .cpp file!!! +#include "tier0/memdbgon.h" + +// In other C files. +// Globals +extern IFileSystem *g_pFileSystem; + +// This is the initial capacity for sentences, the array will grow if necessary +#define MAX_EXPECTED_SENTENCES 900 + +CUtlVector<sentence_t> g_Sentences; +// FIXME: could get this through common includes +const char *COM_Parse (const char *data); +extern char com_token[1024]; + +// Module Locals +static char *rgpparseword[CVOXWORDMAX]; // array of pointers to parsed words +static char voxperiod[] = "_period"; // vocal pause +static char voxcomma[] = "_comma"; // vocal pause + +#define CVOXMAPNAMESMAX 24 +static char *g_rgmapnames[CVOXMAPNAMESMAX]; +static int g_cmapnames = 0; + +// Sentence file list management +static void VOX_ListClear( void ); +static int VOX_ListFileIsLoaded( const char *psentenceFileName ); +static void VOX_ListMarkFileLoaded( const char *psentenceFileName ); +static void VOX_InitAllEntnames( void ); + +void VOX_LookupMapnames( void ); + +static void VOX_Reload() +{ + VOX_Shutdown(); + VOX_Init(); +} +static ConCommand vox_reload( "vox_reload", VOX_Reload, "Reload sentences.txt file", FCVAR_CHEAT ); + +static CUtlVector<unsigned char> g_GroupLRU; +static CUtlVector<char> g_SentenceFile; + +struct sentencegroup_t +{ + short count; + +public: + short lru; + const char *GroupName() const; + CUtlSymbol GroupNameSymbol() const; + void SetGroupName( const char *pName ); + static CUtlSymbol GetSymbol( const char *pName ); + +private: + CUtlSymbol groupname; + static CUtlSymbolTable s_SymbolTable; +}; + +const char *sentencegroup_t::GroupName() const +{ + return s_SymbolTable.String( groupname ); +} + +void sentencegroup_t::SetGroupName( const char *pName ) +{ + groupname = s_SymbolTable.AddString( pName ); +} + +CUtlSymbol sentencegroup_t::GroupNameSymbol() const +{ + return groupname; +} + +CUtlSymbol sentencegroup_t::GetSymbol( const char *pName ) +{ + return s_SymbolTable.AddString( pName ); +} + +CUtlVector<sentencegroup_t> g_SentenceGroups; +CUtlSymbolTable sentencegroup_t::s_SymbolTable( 0, 256, true ); + +struct WordBuf +{ + WordBuf() + { + word[ 0 ] = 0; + } + + WordBuf( const WordBuf& src ) + { + Q_strncpy( word, src.word, sizeof( word ) ); + } + + void Set( char const *w ) + { + if ( !w ) + { + word[ 0 ] = 0; + return; + } + Q_strncpy( word, w, sizeof( word ) ); + while ( Q_strlen( word ) >= 1 && word[ Q_strlen( word ) - 1 ] == ' ' ) + { + word[ Q_strlen( word ) - 1 ] = 0; + } + } + + char word[ 256 ]; +}; + +struct ccpair +{ + WordBuf token; + WordBuf value; + + WordBuf fullpath; +}; + +static void VOX_BuildVirtualNameList( char *word, CUtlVector< WordBuf >& list ); + +// This module depends on these engine calls: +// DevMsg +// S_FreeChannel +// S_LoadSound +// S_FindName +// It also depends on vstdlib/RandomInt (all other random calls go through g_pSoundServices) + +void VOX_Init( void ) +{ + VOX_InitAllEntnames(); + + g_SentenceFile.Purge(); + g_GroupLRU.Purge(); + g_Sentences.RemoveAll(); + g_Sentences.EnsureCapacity( MAX_EXPECTED_SENTENCES ); + + VOX_ListClear(); + + VOX_ReadSentenceFile( "scripts/sentences.txt" ); + VOX_LookupMapnames(); +} + + +void VOX_Shutdown( void ) +{ + g_Sentences.RemoveAll(); + VOX_ListClear(); + g_SentenceGroups.RemoveAll(); + g_cmapnames = 0; +} + +//----------------------------------------------------------------------------- +// Purpose: This is kind of like strchr(), but we get the actual pointer to the +// end of the string when it fails rather than NULL. This is useful +// for parsing buffers containing multiple strings +// Input : *string - +// scan - +// Output : char +//----------------------------------------------------------------------------- +char *ScanForwardUntil( char *string, char scan ) +{ + while( string[0] ) + { + if ( string[0] == scan ) + return string; + + string++; + } + return string; +} + +// parse a null terminated string of text into component words, with +// pointers to each word stored in rgpparseword +// note: this code actually alters the passed in string! + +char **VOX_ParseString(char *psz) +{ + int i; + int fdone = 0; + char *pszscan = psz; + char c; + characterset_t nextWord, skip; + + memset(rgpparseword, 0, sizeof(char *) * CVOXWORDMAX); + + if (!psz) + return NULL; + + i = 0; + rgpparseword[i++] = psz; + + CharacterSetBuild( &nextWord, " ,.({" ); + CharacterSetBuild( &skip, "., " ); + while (!fdone && i < CVOXWORDMAX) + { + // scan up to next word + c = *pszscan; + while (c && !IN_CHARACTERSET(nextWord,c) ) + c = *(++pszscan); + + // if '(' then scan for matching ')' + if ( c == '(' || c=='{' ) + { + if ( c == '(' ) + pszscan = ScanForwardUntil( pszscan, ')' ); + else if ( c == '{' ) + pszscan = ScanForwardUntil( pszscan, '}' ); + + c = *(++pszscan); + if (!c) + fdone = 1; + } + + if (fdone || !c) + fdone = 1; + else + { + // if . or , insert pause into rgpparseword, + // unless this is the last character + if ((c == '.' || c == ',') && *(pszscan+1) != '\n' && *(pszscan+1) != '\r' + && *(pszscan+1) != 0) + { + if (c == '.') + rgpparseword[i++] = voxperiod; + else + rgpparseword[i++] = voxcomma; + + if (i >= CVOXWORDMAX) + break; + } + + // null terminate substring + *pszscan++ = 0; + + // skip whitespace + c = *pszscan; + while (c && IN_CHARACTERSET(skip, c)) + c = *(++pszscan); + + if (!c) + fdone = 1; + else + rgpparseword[i++] = pszscan; + } + } + return rgpparseword; +} + +// backwards scan psz for last '/' +// return substring in szpath null terminated +// if '/' not found, return 'vox/' + +char *VOX_GetDirectory(char *szpath, int maxpath, char *psz) +{ + char c; + int cb = 0; + char *pszscan = psz + Q_strlen( psz ) - 1; + + // scan backwards until first '/' or start of string + c = *pszscan; + while (pszscan > psz && c != '/') + { + c = *(--pszscan); + cb++; + } + + if (c != '/') + { + // didn't find '/', return default directory + Q_strncpy(szpath, "vox/", maxpath ); + return psz; + } + + cb = Q_strlen(psz) - cb; + + cb = clamp( cb, 0, maxpath - 1 ); + + // FIXME: Is this safe? + Q_memcpy(szpath, psz, cb); + szpath[cb] = 0; + return pszscan + 1; +} + +// get channel volume scale if word + +#ifndef SWDS +float VOX_GetChanVol(channel_t *ch) +{ + if ( !ch->pMixer ) + return 1.0; + + return ch->pMixer->GetVolumeScale(); +/* + + if ( scale == 1.0 ) + return; + + ch->rightvol = (int) (ch->rightvol * scale); + ch->leftvol = (int) (ch->leftvol * scale); + + if ( g_AudioDevice->Should3DMix() ) + { + ch->rrightvol = (int) (ch->rrightvol * scale); + ch->rleftvol = (int) (ch->rleftvol * scale); + ch->centervol = (int) (ch->centervol * scale); + } + else + { + ch->rrightvol = 0; + ch->rleftvol = 0; + ch->centervol = 0; + } +*/ +} +#endif + +//=============================================================================== +// Get any pitch, volume, start, end params into voxword +// and null out trailing format characters +// Format: +// someword(v100 p110 s10 e20) +// +// v is volume, 0% to n% +// p is pitch shift up 0% to n% +// s is start wave offset % +// e is end wave offset % +// t is timecompression % +// +// pass fFirst == 1 if this is the first string in sentence +// returns 1 if valid string, 0 if parameter block only. +// +// If a ( xxx ) parameter block does not directly follow a word, +// then that 'default' parameter block will be used as the default value +// for all following words. Default parameter values are reset +// by another 'default' parameter block. Default parameter values +// for a single word are overridden for that word if it has a parameter block. +// +//=============================================================================== + +int VOX_ParseWordParams(char *psz, voxword_t *pvoxword, int fFirst) +{ + char *pszsave = psz; + char c; + char ct; + char sznum[8]; + int i; + static voxword_t voxwordDefault; + characterset_t commandSet, delimitSet; + + // List of valid commands + CharacterSetBuild( &commandSet, "vpset)" ); + + // init to defaults if this is the first word in string. + if (fFirst) + { + voxwordDefault.pitch = -1; + voxwordDefault.volume = 100; + voxwordDefault.start = 0; + voxwordDefault.end = 100; + voxwordDefault.fKeepCached = 0; + voxwordDefault.timecompress = 0; + } + + *pvoxword = voxwordDefault; + + // look at next to last char to see if we have a + // valid format: + + c = *(psz + strlen(psz) - 1); + + if (c != ')') + return 1; // no formatting, return + + // scan forward to first '(' + CharacterSetBuild( &delimitSet, "()" ); + c = *psz; + while ( !IN_CHARACTERSET(delimitSet, c) ) + c = *(++psz); + + if ( c == ')' ) + return 0; // bogus formatting + + // null terminate + + *psz = 0; + ct = *(++psz); + + while (1) + { + // scan until we hit a character in the commandSet + + while (ct && !IN_CHARACTERSET(commandSet, ct) ) + ct = *(++psz); + + if (ct == ')') + break; + + memset(sznum, 0, sizeof(sznum)); + i = 0; + + c = *(++psz); + + if (!V_isdigit(c)) + break; + + // read number + while (V_isdigit(c) && i < sizeof(sznum) - 1) + { + sznum[i++] = c; + c = *(++psz); + } + + // get value of number + i = atoi(sznum); + + switch (ct) + { + case 'v': pvoxword->volume = i; break; + case 'p': pvoxword->pitch = i; break; + case 's': pvoxword->start = i; break; + case 'e': pvoxword->end = i; break; + case 't': pvoxword->timecompress = i; break; + } + + ct = c; + } + + // if the string has zero length, this was an isolated + // parameter block. Set default voxword to these + // values + + if (strlen(pszsave) == 0) + { + voxwordDefault = *pvoxword; + return 0; + } + else + return 1; +} + +#define CVOXSAVEDWORDSIZE 32 + +// saved entity name/number based on type of entity & id + +#define CVOXGLOBMAX 4 // max number of rnd and seqential globals + +typedef struct _vox_entname +{ + // type is defined by last character of group name. + // for instance, V_MYNAME_S has type 'S', which is used for soldiers + // V_MYNUM_M has type 'P' which is used for metrocops + + int type; + + SoundSource soundsource; // the enity emitting the sentence + char *pszname; // a custom name for the entity (this is a word name) + char *psznum; // a custom number for the entity (this is a word name) + char *pszglobal[CVOXGLOBMAX]; // 1 global word, shared by this type of entity, picked randomly, expires after 5min + char *pszglobalseq[CVOXGLOBMAX]; // 1 global word, shared by this type of entity, picked in sequence, expires after 5 min + bool fdied; // true if ent died (don't clear, we need its name) + int iseq[CVOXGLOBMAX]; // sequence index, for global sequential lookups + float timestamp[CVOXGLOBMAX]; // latest update to this ent global timestamp + float timestampseq[CVOXGLOBMAX]; // latest update to this ent global sequential timestamp + float timedied; // timestamp of death + +} vox_entname; + +#define CENTNAMESMAX 64 + +vox_entname g_entnames[CENTNAMESMAX]; + +int g_entnamelastsaved = 0; + +// init all + +void VOX_InitAllEntnames( void ) +{ + g_entnamelastsaved = 0; + Q_memset(g_entnames, 0, sizeof(g_entnames)); + Q_memset(g_rgmapnames, 0, sizeof(g_rgmapnames)); + g_cmapnames = 0; +} + +// get new index + +int VOX_GetNextEntnameIndex( void ) +{ + g_entnamelastsaved++; + + if (g_entnamelastsaved >= CENTNAMESMAX) + { + g_entnamelastsaved = 0; + } + + return g_entnamelastsaved; +} + +// get index of this ent, or get a new index. if fallocnew is true, +// get a new slot if none found. +// NOTE: this routine always sets fdied to false - fdied is later +// set to true by the caller if in IDIED routine. This +// ensures that if an ent is reused, it won't be marked as fdied. + +int VOX_LookupEntIndex( int type, SoundSource soundsource, bool fallocnew) +{ + int i; + + for (i = 0; i < CENTNAMESMAX; i++) + { + if ((g_entnames[i].type == type) && (g_entnames[i].soundsource == soundsource)) + { + g_entnames[i].fdied = false; + return i; + } + } + + if ( !fallocnew ) + return -1; + + // new index slot - init + + int inew = VOX_GetNextEntnameIndex(); + + g_entnames[inew].type = type; + g_entnames[inew].soundsource = soundsource; + g_entnames[inew].timedied = 0; + g_entnames[inew].fdied = 0; + g_entnames[inew].pszname = NULL; + g_entnames[inew].psznum = NULL; + + for (i = 0; i < CVOXGLOBMAX; i++) + { + g_entnames[inew].pszglobal[i] = NULL; + g_entnames[inew].timestamp[i] = 0; + g_entnames[inew].iseq[i] = 0; + g_entnames[inew].timestampseq[i] = 0; + g_entnames[inew].pszglobalseq[i] = NULL; + } + + return inew; +} + +// lookup random first word from this named group, +// return static, null terminated string + +char * VOX_LookupRndVirtual( char *pGroupName ) +{ + // get group index + + int isentenceg = VOX_GroupIndexFromName( pGroupName ); + + if ( isentenceg < 0) + return NULL; + + char szsentencename[32]; + + // get pointer to sentence name within group, using lru + + int isentence = VOX_GroupPick( isentenceg, szsentencename, sizeof(szsentencename)-1 ); + + if (isentence < 0) + return NULL; + + // get pointer to sentence data + + char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); + + // strip trailing whitespace + + if (!psz) + return NULL; + + char *pend = Q_strstr(psz, " "); + if (pend) + *pend = 0; + + // return pointer to first (and only) word + + return psz; +} + +// given groupname, get pointer to first word of n'th sentence in group + +char *VOX_LookupSentenceByIndex( char *pGroupname, int ipick, int *pipicknext ) +{ + // get group index + + int isentenceg = VOX_GroupIndexFromName( pGroupname ); + + if ( isentenceg < 0) + return NULL; + + char szsentencename[32]; + + // get pointer to sentence name within group, using lru + + int isentence = VOX_GroupPickSequential( isentenceg, szsentencename, sizeof(szsentencename)-1, ipick, true ); + + if (isentence < 0) + return NULL; + + // get pointer to sentence data + + char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); + + // strip trailing whitespace + + char *pend = Q_strstr(psz, " "); + if (pend) + *pend = 0; + + if (pipicknext) + *pipicknext = isentence; + + // return pointer to first (and only) word + return psz; +} + +// lookup first word from this named group, group entry 'ipick', +// return static, null terminated string + +char * VOX_LookupNumber( char *pGroupName, int ipick ) +{ + // construct group name from V_NUMBERS + TYPE + + char sznumbers[16]; + int glen = Q_strlen(pGroupName); + int slen = Q_strlen("V_NUMBERS"); + + V_strcpy_safe(sznumbers, "V_NUMBERS"); + + // insert type character + sznumbers[slen] = pGroupName[glen-1]; + sznumbers[slen+1] = 0; + + return VOX_LookupSentenceByIndex( sznumbers, ipick, NULL ); +} + +// lookup ent & type, return static, null terminated string +// if no saved string, create one. +// UNDONE: init ent/type/string array, wrap when saving + +char * VOX_LookupMyVirtual( int iname, char *pGroupName, char chtype, SoundSource soundsource) +{ + char *psz = NULL; + char **ppsz = NULL; + + // get existing ent index, or index to new slot + + int ient = VOX_LookupEntIndex( (int)chtype, soundsource, true ); + + if (iname == 1) + { + // lookup saved name + + psz = g_entnames[ient].pszname; + ppsz = &(g_entnames[ient].pszname); + } + else + { + // lookup saved number + + psz = g_entnames[ient].psznum; + ppsz = &(g_entnames[ient].psznum); + } + + // if none found for this ent - pick one and save it + + if (psz == NULL) + { + // get new string + psz = VOX_LookupRndVirtual( pGroupName ); + + // save pointer to new string in g_entnames + *ppsz = psz; + } + + return psz; +} + +// get range or heading from ent to player, +// store range in from 1 to 3 words as ppszNew...ppszNew2 +// store count of words in pcnew +// if fsimple is true, return numeric sequence based on ten digit max + +void VOX_LookupRangeHeadingOrGrid( int irhg, char *pGroupName, channel_t *pChannel, SoundSource soundsource, char **ppszNew, char **ppszNew1, char **ppszNew2, int *pcnew, bool fsimple ) +{ + Vector SL; // sound -> listener vector + char *phundreds = NULL; + char *ptens = NULL; + char *pones = NULL; + int cnew = 0; + float dist; + int dmeters = 0; + int hundreds, tens, ones; + + VectorSubtract(listener_origin, pChannel->origin, SL); + + if (irhg == 0) + { + // get range + dist = VectorLength(SL); + + dmeters = (int)((dist * 2.54 / 100.0)); // convert inches to meters + + dmeters = clamp(dmeters, 0, 900); + } + else if (irhg == 1) + { + // get heading + QAngle source_angles; + + source_angles.Init(0.0, 0.0, 0.0); + + VectorAngles( SL, source_angles ); + + dmeters = source_angles[YAW]; + } else if (irhg == 2) + { + // get gridx + dmeters = (int)(((16384 + listener_origin.x) * 2.54 / 100.0) / 10) % 20; + } + else if (irhg == 3) + { + // get gridy + dmeters = (int)(((16384 + listener_origin.y) * 2.54 / 100.0) / 10) % 20; + } + + dmeters = clamp(dmeters, 0, 999); + + // get hundreds, tens, ones + + hundreds = dmeters / 100; + tens = (dmeters - hundreds * 100) / 10; + ones = (dmeters - hundreds * 100 - tens * 10); + + + if (fsimple) + { + // just return simple ten digit lookups for ones, tens, hundreds + + pones = VOX_LookupNumber( pGroupName, ones); + cnew++; + + if (tens || hundreds) + { + ptens = VOX_LookupNumber( pGroupName, tens); + cnew++; + } + + if (hundreds) + { + phundreds = VOX_LookupNumber( pGroupName, hundreds ); + cnew++; + } + + goto LookupNumExit; + } + + // get pointer to string from groupname and number + + // 100,200,300,400,500,600,700,800,900 + if (hundreds && !tens && !ones) + { + if (hundreds <= 3) + { + phundreds = VOX_LookupNumber( pGroupName, 27 + hundreds); + cnew++; + } + else + { + phundreds = VOX_LookupNumber( pGroupName, hundreds ); + ptens = VOX_LookupNumber( pGroupName, 0); + pones = VOX_LookupNumber( pGroupName, 0); + cnew++; + cnew++; + + } + goto LookupNumExit; + } + + + if ( hundreds ) + { + // 101..999 + if (hundreds <= 3 && !tens && ones) + phundreds = VOX_LookupNumber( pGroupName, 27 + hundreds); + else + phundreds = VOX_LookupNumber( pGroupName, hundreds ); + + cnew++; + + // 101..109 to 901..909 + if (!tens && ones) + { + pones = VOX_LookupNumber( pGroupName, ones); + cnew++; + if (hundreds > 3) + { + ptens = VOX_LookupNumber( pGroupName, 0); + cnew++; + } + goto LookupNumExit; + } + } + + // 1..19 + if (tens <= 1 && (tens || ones)) + { + pones = VOX_LookupNumber( pGroupName, ones + tens * 10 ); + cnew++; + tens = 0; + goto LookupNumExit; + } + + // 20..99 + if (tens > 1) + { + if (ones) + { + pones = VOX_LookupNumber( pGroupName, ones ); + cnew++; + } + + ptens = VOX_LookupNumber( pGroupName, 18 + tens); + cnew++; + } + + +LookupNumExit: + // return values + + *pcnew = cnew; + + // return + switch (cnew) + { + default: + *ppszNew = NULL; + return; + case 1: // 1..19,20,30,40,50,60,70,80,90,100,200,300 + *ppszNew = pones ? pones : (ptens ? ptens : (phundreds ? phundreds : NULL)); + return; + case 2: + if (ptens && pones) + { + *ppszNew = ptens; + *ppszNew1 = pones; + } + else if (phundreds && pones) + { + *ppszNew = phundreds; + *ppszNew1 = pones; + } + else if (phundreds && ptens) + { + *ppszNew = phundreds; + *ppszNew1 = ptens; + } + return; + case 3: + *ppszNew = phundreds; + *ppszNew1 = ptens; + *ppszNew2 = pones; + return; + } +} + +// find most recent ent of this type marked as dead + +int VOX_LookupLastDeadIndex( int type ) +{ + float timemax = -1; + int ifound = -1; + int i; + + for (i = 0; i < CENTNAMESMAX; i++) + { + if (g_entnames[i].type == type && g_entnames[i].fdied) + { + if (g_entnames[i].timedied >= timemax) + { + timemax = g_entnames[i].timedied; + ifound = i; + } + } + } + + return ifound; +} + +ConVar snd_vox_globaltimeout("snd_vox_globaltimeout", "300"); // n second timeout to reset global vox words +ConVar snd_vox_seqtimeout("snd_vox_seqtimetout", "300"); // n second timeout to reset global sequential vox words +ConVar snd_vox_sectimeout("snd_vox_sectimetout", "300"); // n second timeout to reset global sector id +ConVar snd_vox_captiontrace( "snd_vox_captiontrace", "0", 0, "Shows sentence name for sentences which are set not to show captions." ); + +// return index to ent which knows the current sector. +// if no ent found, alloc a new one and establish shector. +// sectors expire after approx 5 minutes. + +#define VOXSECTORMAX 20 + +static float g_vox_lastsectorupdate = 0; +static int g_vox_isector = -1; + +char *VOX_LookupSectorVirtual( char *pGroupname ) +{ + float curtime = g_pSoundServices->GetClientTime(); + + if (g_vox_isector == -1) + { + g_vox_isector = RandomInt(0, VOXSECTORMAX-1); + } + +// update sector every 5 min + + if (curtime - g_vox_lastsectorupdate > snd_vox_sectimeout.GetInt()) + { + g_vox_isector++; + if (g_vox_isector > VOXSECTORMAX) + g_vox_isector = 1; + g_vox_lastsectorupdate = curtime; + } + + return VOX_LookupNumber( pGroupname, g_vox_isector ); +} + + + +char *VOX_LookupGlobalVirtual( int type, SoundSource soundsource, char *pGroupName, int iglobal ) +{ + int i; + float curtime = g_pSoundServices->GetClientTime(); + + // look for ent of this type with un-expired global + + for (i = 0; i < CENTNAMESMAX; i++) + { + if (g_entnames[i].type == type) + { + if (curtime - g_entnames[i].timestamp[iglobal] <= snd_vox_globaltimeout.GetInt()) + { + // if this ent has an un-expired global, return it, otherwise break + + if (g_entnames[i].pszglobal[iglobal]) + return g_entnames[i].pszglobal[iglobal]; + else + break; + } + } + } + + // if not found, construct a new global for this ent + + // pick random word from groupname + + char *psz = VOX_LookupRndVirtual( pGroupName ); + + // get existing ent index, or index to new slot + + int ient = VOX_LookupEntIndex( type, soundsource, true ); + + g_entnames[ient].timestamp[iglobal] = curtime; + g_entnames[ient].pszglobal[iglobal] = psz; + + return psz; +} + +// lookup global values in group in sequence - get next value +// in sequence. sequence counter expires every 2.5 minutes. + +char *VOX_LookupGlobalSeqVirtual( int type, SoundSource soundsource, char *pGroupName, int iglobal ) +{ + + int i; + int ient; + float curtime = g_pSoundServices->GetClientTime(); + + // look for ent of this type with un-expired global + + for (i = 0; i < CENTNAMESMAX; i++) + { + if (g_entnames[i].type == type) + { + if (curtime - g_entnames[i].timestampseq[iglobal] <= (snd_vox_seqtimeout.GetInt()/2)) + { + // if first ent found has an un-expired global sequence set, + // get next value in sequence, otherwise break + + ient = i; + goto Pick_next; + } + else + { + // global has expired - reset sequence + + ient = i; + g_entnames[ient].iseq[iglobal] = 0; + goto Pick_next; + } + } + } + + // if not found, construct a new sequential global for this ent + + ient = VOX_LookupEntIndex( type, soundsource, true ); + + // pick next word from groupname +Pick_next: + int ipick = g_entnames[ient].iseq[iglobal]; + int ipicknext = 0; + + char *psz = VOX_LookupSentenceByIndex( pGroupName, ipick, &ipicknext ); + g_entnames[ient].iseq[iglobal] = ipicknext; + + // get existing ent index, or index to new slot + + g_entnames[ient].timestampseq[iglobal] = curtime; + g_entnames[ient].pszglobalseq[iglobal] = psz; + + return psz; +} + +// insert new words into rgpparseword at 'ireplace' slot + +void VOX_InsertWords( int ireplace, int cnew, char *pszNew, char *pszNew1, char *pszNew2 ) +{ + if ( cnew ) + { + // make space in rgpparseword for 'cnew - 1' new words + int ccopy = cnew - 1; // number of new slots we need + int j; + + if (ccopy) + { + for (j = CVOXWORDMAX-1; j > ireplace + ccopy; j--) + rgpparseword[j] = rgpparseword[j - ccopy ]; + } + + // replace rgpparseword entry(s) with the substitued name(s) + + rgpparseword[ireplace] = pszNew; + + if ( cnew == 2 || cnew == 3) + rgpparseword[ireplace+1] = pszNew1; + + if ( cnew == 3 ) + rgpparseword[ireplace+2] = pszNew2; + } +} + +// remove 'silent' word from rgpparseword + +void VOX_DeleteWord( int iword ) +{ + if (iword < 0 || iword >= CVOXWORDMAX) + return; + + rgpparseword[iword] = 0; + + // slide all words > iword up into vacated slot + + for (int j = iword; j < CVOXWORDMAX-1; j++) + rgpparseword[j] = rgpparseword[j+1]; +} + + +// get global list of map names from sentences.txt +// map names are stored in order in V_MAPNAMES group + +void VOX_LookupMapnames( void ) +{ + // get group V_MAPNAMES + + int i; + char *psz; + int inext = 0; + + for (i = 0; i < CVOXMAPNAMESMAX; i++) + { + // step sequentially through group - return ptr to 1st word in each group (map name) + + psz = VOX_LookupSentenceByIndex( "V_MAPNAME", i, &inext ); + + if (!psz) + return; + + g_rgmapnames[i] = psz; + g_cmapnames++; + } +} + +// get index of current map name +// return 0 as default index if not found + +int VOX_GetMapNameIndex( const char *pszmapname ) +{ + for (int i = 0; i < g_cmapnames; i++) + { + if ( Q_strstr( pszmapname, g_rgmapnames[i] ) ) + return i; + } + return 0; +} + +// look for virtual 'V_' values in rgpparseword. + // V_MYNAME - replace with saved name value (based on type + entity) + // - if no saved name, create one and save + // V_MYNUM - replace with saved number value (based on type + entity) + // - if no saved num, create on and save + // V_RNDNUM - grab a random number string from V_RNDNUM_<type> + // V_RNDNAME - grab a random name string from V_RNDNAME_<type> + + // replace any 'V_' values with actual string names in rgpparseword + +extern ConVar host_map; +inline bool IsVirtualName( const char *pName ) +{ + return (pName[0] == 'V' && pName[1] == '_'); +} + +void VOX_ReplaceVirtualNames( channel_t *pchan ) +{ + // for each word in the sentence, check for V_, if found + // replace virtual word with saved word or rnd word + + int i = 0; + char *pszNew = NULL; + char *pszNew1 = NULL; + char *pszNew2 = NULL; + int iname = -1; + int cnew = 0; + bool fbymap; + char *pszmaptoken; + SoundSource soundsource = pchan ? pchan->soundsource : 0; + + const char *pszmap = host_map.GetString(); + + // get global list of map names from sentences.txt + + while (rgpparseword[i]) + { + + if ( IsVirtualName( rgpparseword[i] ) ) + { + iname = -1; + cnew = 0; + pszNew = NULL; + pszNew1 = NULL; + pszNew2 = NULL; + char szparseword[256]; + + int slen = Q_strlen(rgpparseword[i]); + char chtype = rgpparseword[i][slen-1]; + + // copy word to temp location so we can perform in-place substitutions + + V_strcpy_safe(szparseword, rgpparseword[i]); + + // fbymap is true if lookup is performed via mapname instead of via ordinal + + pszmaptoken = ( Q_strstr(szparseword, "_MAP__") ); + + fbymap = (pszmaptoken == NULL ? false : true); + + if (fbymap) + { + int imap = VOX_GetMapNameIndex( pszmap ); + imap = clamp (imap, 0, 99); + + // replace last 2 characters in _MAP__ substring + // with imap - this effectively makes all + // '_map_' lookups relative to the mapname + if ( imap >= 10 ) + { + pszmaptoken[4] = (imap/10) + '0'; + pszmaptoken[5] = (imap%10) + '0'; + } + else + { + pszmaptoken[4] = '0'; + pszmaptoken[5] = imap + '0'; + } + } + + if ( Q_strstr(szparseword, "V_MYNAME") ) + { + iname = 1; + } + else if ( Q_strstr(szparseword, "V_MYNUM") ) + { + iname = 0; + } + + if ( iname >= 0 ) + { + + // lookup ent & type, return static, null terminated string + // if no saved string, create one + + pszNew = VOX_LookupMyVirtual( iname, szparseword, chtype, soundsource); + cnew = 1; + } + else + { + if ( Q_strstr(szparseword, "V_RND") ) + { + // lookup random first word from this named group, + // return static, null terminated string + + pszNew = VOX_LookupRndVirtual( szparseword ); + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_DIST") ) + { + // get range from ent to player, return pointers to new words + VOX_LookupRangeHeadingOrGrid( 0, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); + } + else if ( Q_strstr(szparseword, "V_DIR") ) + { + // get heading from ent to player, return pointers to new words + VOX_LookupRangeHeadingOrGrid( 1, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, false); + } + else if ( Q_strstr(szparseword, "V_IDIED") ) + { + // SILENT MARKER - this ent died - mark as dead and timestamp + + int ient = VOX_LookupEntIndex( chtype, soundsource, false); + if (ient < 0) + { + // if not found, allocate new ent, give him a name & number, mark as dead + char szgroup1[32]; + char szgroup2[32]; + V_strcpy_safe(szgroup1, "V_MYNAME"); + szgroup1[8] = chtype; + szgroup1[9] = 0; + + V_strcpy_safe(szgroup2, "V_MYNUM"); + szgroup2[7] = chtype; + szgroup2[8] = 0; + + ient = VOX_LookupEntIndex( chtype, soundsource, true); + g_entnames[ient].pszname = VOX_LookupRndVirtual( szgroup1 ); + g_entnames[ient].psznum = VOX_LookupRndVirtual( szgroup2 ); + } + + g_entnames[ient].fdied = true; + g_entnames[ient].timedied = g_pSoundServices->GetClientTime(); + + // clear this 'silent' word from rgpparseword + + VOX_DeleteWord(i); + + } + else if ( Q_strstr(szparseword, "V_WHODIED") ) + { + // get last dead unit of this type + + int ient = VOX_LookupLastDeadIndex( chtype ); + + // get name and number + + if (ient >= 0) + { + cnew = 1; + pszNew = g_entnames[ient].pszname; + pszNew1 = g_entnames[ient].psznum; + if (pszNew1) + cnew++; + } + else + { + // no dead units, just clear V_WHODIED + + VOX_DeleteWord(i); + } + + } + else if ( Q_strstr(szparseword, "V_SECTOR") ) + { + // sectors are fictional - they simply + // increase sequentially and expire every 5 minutes + + pszNew = VOX_LookupSectorVirtual( szparseword ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_GRIDX") ) + { + // player x position in 10 meter increments + VOX_LookupRangeHeadingOrGrid( 2, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); + } + else if ( Q_strstr(szparseword, "V_GRIDY") ) + { + // player y position in 10 meter increments + VOX_LookupRangeHeadingOrGrid( 3, szparseword, pchan, soundsource, &pszNew, &pszNew1, &pszNew2, &cnew, true ); + + } + else if ( Q_strstr(szparseword, "V_G0_") ) + { + // 4 rnd globals per type, globals expire after 5 minutes + // used for target designation, master sector code name etc. + + pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 0 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_G1_") ) + { + // 4 rnd globals per type, globals expire after 5 minutes + // used for target designation, master sector code name etc. + + pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 1 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_G2_") ) + { + // 4 rnd globals per type, globals expire after 5 minutes + // used for target designation, master sector code name etc. + + pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 2 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_G3_") ) + { + // 4 rnd globals per type, globals expire after 5 minutes + // used for target designation, master sector code name etc. + + pszNew = VOX_LookupGlobalVirtual( chtype, soundsource, szparseword, 3 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_SEQG0_") ) + { + // 4 sequential globals per type, selected sequentially in list + // used for total target hit count etc. + + pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 0 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_SEQG1_") ) + { + // 4 sequential globals per type, selected sequentially in list + // used for total target hit count etc. + + pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 1 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_SEQG2_") ) + { + // 4 sequential globals per type, selected sequentially in list + // used for total target hit count etc. + + pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 2 ); + if (pszNew) + cnew = 1; + } + else if ( Q_strstr(szparseword, "V_SEQG3_") ) + { + // 4 sequential globals per type, selected sequentially in list + // used for total target hit count etc. + + pszNew = VOX_LookupGlobalSeqVirtual( chtype, soundsource, szparseword, 3 ); + if (pszNew) + cnew = 1; + } + + } + + // insert up to 3 new words into rgpparseword at 'i' location + + VOX_InsertWords( i, cnew, pszNew, pszNew1, pszNew2 ); + } + i++; + } +} + +void VOX_Precache( IEngineSound *pSoundSystem, int sentenceIndex, const char *pPathOverride = NULL ) +{ + voxword_t rgvoxword[CVOXWORDMAX]; + char buffer[512]; + char szpath[MAX_PATH]; + char pathbuffer[MAX_PATH]; + char *pWords[CVOXWORDMAX]; // array of pointers to parsed words + + if ( !IsVirtualName(g_Sentences[sentenceIndex].pName)) + { + g_Sentences[sentenceIndex].isPrecached = true; + } + + memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); + char *psz = (char *)(g_Sentences[sentenceIndex].pName + Q_strlen(g_Sentences[sentenceIndex].pName) + 1); + // get directory from string, advance psz + psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); + Q_strncpy(buffer, psz, sizeof( buffer ) ); + psz = buffer; + if ( pPathOverride ) + { + Q_strncpy(szpath, pPathOverride, sizeof(szpath)); + } + + // parse sentence (also inserts null terminators between words) + + VOX_ParseString(psz); + int i = 0, count = 0; + // copy the parsed words out of the globals + for ( i = 0; rgpparseword[i]; i++ ) + { + pWords[i] = rgpparseword[i]; + count++; + } + int cword = 0; + for ( i = 0; i < count; i++ ) + { + if ( IsVirtualName(pWords[i]) ) + { + CUtlVector< WordBuf > list; + + VOX_BuildVirtualNameList( pWords[i], list ); + + int c = list.Count(); + for ( int j = 0 ; j < c; ++j ) + { + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, list[j].word ); + pSoundSystem->PrecacheSound( pathbuffer, false ); + } + } + else + { + // Get any pitch, volume, start, end params into voxword + if (VOX_ParseWordParams(pWords[i], &rgvoxword[cword], i == 0)) + { + // this is a valid word (as opposed to a parameter block) + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, pWords[i] ); + // find name, if already in cache, mark voxword + // so we don't discard when word is done playing + pSoundSystem->PrecacheSound( pathbuffer, false ); + cword++; + } + } + } +} + +void VOX_PrecacheSentenceGroup( IEngineSound *pSoundSystem, const char *pGroupName, const char *pPathOverride ) +{ + int i; + + int len = Q_strlen( pGroupName ); + for ( i = 0; i < g_Sentences.Count(); i++ ) + { + if ( !g_Sentences[i].isPrecached && !Q_strncasecmp( g_Sentences[i].pName, pGroupName, len ) ) + { + VOX_Precache( pSoundSystem, i, pPathOverride ); + } + } +} + + +// link all sounds in sentence, start playing first word. +// return number of words loaded +void VOX_LoadSound( channel_t *pchan, const char *pszin ) +{ +#ifndef SWDS + char buffer[512]; + int i, cword; + char pathbuffer[MAX_PATH]; + char szpath[MAX_PATH]; + voxword_t rgvoxword[CVOXWORDMAX]; + char *psz; + bool emitcaption = false; + CUtlSymbol captionSymbol = UTL_INVAL_SYMBOL; + float duration = 0.0f; + + if (!pszin) + return; + + memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); + memset(buffer, 0, sizeof(buffer)); + + // lookup actual string in g_Sentences, + // set pointer to string data + + psz = VOX_LookupString(pszin, NULL, &emitcaption, &captionSymbol, &duration ); + + if (!psz) + { + DevMsg ("VOX_LoadSound: no sentence named %s\n",pszin); + return; + } + + // get directory from string, advance psz + psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); + + if ( Q_strlen(psz) > sizeof(buffer) - 1 ) + { + DevMsg ("VOX_LoadSound: sentence is too long %s\n",psz); + return; + } + + // copy into buffer + Q_strncpy(buffer, psz, sizeof( buffer ) ); + psz = buffer; + + // parse sentence (also inserts null terminators between words) + + VOX_ParseString(psz); + + // replace any 'V_' values with actual string names in rgpparseword + + VOX_ReplaceVirtualNames( pchan ); + + // for each word in the sentence, construct the filename, + // lookup the sfx and save each pointer in a temp array + + i = 0; + cword = 0; + + char captionstream[ 1024 ]; + + char groupname[ 512 ]; + Q_strncpy( groupname, pszin, sizeof( groupname ) ); + + int len = Q_strlen( groupname ); + + while ( len > 0 && V_isdigit( groupname[ len - 1 ] ) ) + { + groupname[ len - 1 ] = 0; + --len; + } + + Q_snprintf( captionstream, sizeof( captionstream ), "%s ", groupname ); + + while (rgpparseword[i]) + { + // Get any pitch, volume, start, end params into voxword + + if (VOX_ParseWordParams(rgpparseword[i], &rgvoxword[cword], i == 0)) + { + // this is a valid word (as opposed to a parameter block) + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] ); + + // find name, if already in cache, mark voxword + // so we don't discard when word is done playing + rgvoxword[cword].sfx = S_FindName(pathbuffer, + &(rgvoxword[cword].fKeepCached)); + // JAY: HACKHACK: Keep all sentences cached for now + rgvoxword[cword].fKeepCached = 1; + + char captiontoken[ 128 ]; + Q_snprintf( captiontoken, sizeof( captiontoken ), "S(%s%s) ", szpath, rgpparseword[i] ); + + Q_strncat( captionstream, captiontoken, sizeof( captionstream ), COPY_ALL_CHARACTERS ); + + cword++; + } + i++; + } + + pchan->pMixer = NULL; + + if (cword) + { + // some 'virtual' sentences can end up with 0 words + // if no words, then pchan->pMixer is null; chan will be released right away. + + pchan->pMixer = CreateSentenceMixer( rgvoxword ); + if ( !pchan->pMixer ) + return; + + pchan->flags.isSentence = true; + pchan->sfx = rgvoxword[0].sfx; + Assert(pchan->sfx); + + if ( g_pSoundServices ) + { + if ( emitcaption ) + { + if ( captionSymbol != UTL_INVAL_SYMBOL ) + { + g_pSoundServices->EmitCloseCaption( captionSymbol.String(), duration ); + + if ( snd_vox_captiontrace.GetBool() ) + { + Msg( "Vox: caption '%s'\n", captionSymbol.String() ); + } + } + else + { + g_pSoundServices->EmitSentenceCloseCaption( captionstream ); + + if ( snd_vox_captiontrace.GetBool() ) + { + Msg( "Vox: captionstream '%s'\n", captionstream ); + } + } + } + else + { + if ( snd_vox_captiontrace.GetBool() ) + { + Msg( "Vox: No caption for '%s'\n", pszin ? pszin : "NULL" ); + } + } + } + } + +#endif +} + +static bool CCPairLessFunc( const ccpair& lhs, const ccpair& rhs ) +{ + return Q_stricmp( lhs.token.word, rhs.token.word ) < 0; +} + +void VOX_AddNumbers( char *pGroupName, CUtlVector< WordBuf >& list ) +{ + // construct group name from V_NUMBERS + TYPE + for ( int i = 0; i <= 30; ++i ) + { + char sznumbers[16]; + int glen = Q_strlen(pGroupName); + int slen = Q_strlen("V_NUMBERS"); + + V_strcpy_safe(sznumbers, "V_NUMBERS"); + + // insert type character + sznumbers[slen] = pGroupName[glen-1]; + sznumbers[slen+1] = 0; + + WordBuf w; + // w.Set( VOX_LookupString( VOX_LookupSentenceByIndex( sznumbers, i, NULL ), NULL ) ); + w.Set( VOX_LookupSentenceByIndex( sznumbers, i, NULL ) ); + list.AddToTail( w ); + } +} + +void VOX_AddRndVirtual( char *pGroupName, CUtlVector< WordBuf >& list ) +{ + // get group index + + int isentenceg = VOX_GroupIndexFromName( pGroupName ); + + if ( isentenceg < 0) + return; + + char szsentencename[32]; + + char const *szgroupname = g_SentenceGroups[ isentenceg ].GroupName(); + + // get pointer to sentence name within group, using lru + for ( int snum = 0; snum < g_SentenceGroups[ isentenceg ].count; ++snum ) + { + Q_snprintf( szsentencename, sizeof( szsentencename ), "%s%d", szgroupname, snum ); + + char *psz = VOX_LookupString( szsentencename[0] == '!' ? szsentencename+1 : szsentencename, NULL); + + if ( psz ) + { + WordBuf w; + w.Set( psz ); + list.AddToTail( w ); + } + } +} + +void VOX_AddMyVirtualWords( int iname, char *pGroupName, char chtype, CUtlVector< WordBuf >& list ) +{ + VOX_AddRndVirtual( pGroupName, list ); +} + +void VOX_BuildVirtualNameList( char *word, CUtlVector< WordBuf >& list ) +{ + // for each word in the sentence, check for V_, if found + // replace virtual word with saved word or rnd word + + int iname = -1; + bool fbymap; + char *pszmaptoken; + + + char szparseword[256]; + + int slen = Q_strlen(word); + char chtype = word[slen-1]; + + // copy word to temp location so we can perform in-place substitutions + + Q_strncpy( szparseword, word, sizeof( szparseword ) ); + + // fbymap is true if lookup is performed via mapname instead of via ordinal + + pszmaptoken = ( Q_strstr(szparseword, "_MAP__") ); + + fbymap = (pszmaptoken == NULL ? false : true); + + if (fbymap) + { + for ( int imap = 0; imap < g_cmapnames; ++imap ) + { + // replace last 2 characters in _MAP__ substring + // with imap - this effectively makes all + // '_map_' lookups relative to the mapname + pszmaptoken[4] = '0'; + if (imap < 10) + Q_snprintf( &(pszmaptoken[5]), 1, "%1d", imap ); + else + Q_snprintf( &(pszmaptoken[4]), 2, "%d", imap ); + + // Recurse... + VOX_BuildVirtualNameList( szparseword, list ); + } + return; + } + + if ( Q_strstr(szparseword, "V_MYNAME") ) + { + iname = 1; + } + else if ( Q_strstr(szparseword, "V_MYNUM") ) + { + iname = 0; + } + + if ( iname >= 0 ) + { + + // lookup ent & type, return static, null terminated string + // if no saved string, create one + + VOX_AddMyVirtualWords( iname, szparseword, chtype, list ); + } + else + { + if ( Q_strstr(szparseword, "V_RND") ) + { + // lookup random first word from this named group, + // return static, null terminated string + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_DIST") ) + { + VOX_AddNumbers( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_DIR") ) + { + VOX_AddNumbers( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_IDIED") ) + { + // SILENT MARKER - this ent died - mark as dead and timestamp + + // if not found, allocate new ent, give him a name & number, mark as dead + char szgroup1[32]; + char szgroup2[32]; + V_strcpy_safe(szgroup1, "V_MYNAME"); + szgroup1[8] = chtype; + szgroup1[9] = 0; + + V_strcpy_safe(szgroup2, "V_MYNUM"); + szgroup2[7] = chtype; + szgroup2[8] = 0; + + VOX_BuildVirtualNameList( szgroup1, list ); + VOX_BuildVirtualNameList( szgroup2, list ); + return; + + } + else if ( Q_strstr(szparseword, "V_WHODIED") ) + { + // get last dead unit of this type + /* + + int ient = VOX_LookupLastDeadIndex( chtype ); + + // get name and number + + if (ient >= 0) + { + cnew = 1; + pszNew = g_entnames[ient].pszname; + pszNew1 = g_entnames[ient].psznum; + if (pszNew1) + cnew++; + } + else + { + // no dead units, just clear V_WHODIED + + VOX_DeleteWord(i); + } + */ + + } + else if ( Q_strstr(szparseword, "V_SECTOR") ) + { + VOX_AddNumbers( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_GRIDX") ) + { + VOX_AddNumbers( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_GRIDY") ) + { + VOX_AddNumbers( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_G0_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_G1_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_G2_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_G3_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_SEQG0_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_SEQG1_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_SEQG2_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + else if ( Q_strstr(szparseword, "V_SEQG3_") ) + { + VOX_AddRndVirtual( szparseword, list ); + } + + } + + if ( Q_strnicmp( szparseword, "V_", 2 ) ) + { + WordBuf w; + w.Set( szparseword ); + list.AddToTail( w ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: For generating reslists, adds the wavefile to the dictionary +// Input : *fn - +//----------------------------------------------------------------------------- +void VOX_Touch( char const *fn, CUtlDict< int, int >& list ) +{ + if ( list.Find( fn ) == list.InvalidIndex() ) + { + list.Insert( fn ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Iterates the touch list and touches all referenced .wav files. +// Input : int - +// list - +//----------------------------------------------------------------------------- +void VOX_TouchSounds( CUtlDict< int, int >& list, CUtlRBTree< ccpair, int >& ccpairs, bool spewsentences ) +{ + int i; + for ( i = list.First(); i != list.InvalidIndex(); i = list.Next( i ) ) + { + char const *fn = list.GetElementName( i ); + + // Msg( "touch %s\n", fn ); + char expanded[ 512 ]; + Q_snprintf( expanded, sizeof( expanded ), "sound/%s", fn ); + + FileHandle_t fh = g_pFileSystem->Open( expanded, "rb" ); + if ( FILESYSTEM_INVALID_HANDLE != fh ) + { + g_pFileSystem->Close( fh ); + } + } + + if ( spewsentences ) + { + for ( i = ccpairs.FirstInorder() ; i != ccpairs.InvalidIndex(); i = ccpairs.NextInorder( i ) ) + { + ccpair& pair = ccpairs[ i ]; + + Msg( "\"%s\"\t\"%s\"\n", + pair.token.word, + pair.value.word ); + } + + FileHandle_t fh = g_pFileSystem->Open( "sentences.m3u", "wt", "GAME" ); + if ( FILESYSTEM_INVALID_HANDLE != fh ) + { + for ( i = ccpairs.FirstInorder() ; i != ccpairs.InvalidIndex(); i = ccpairs.NextInorder( i ) ) + { + ccpair& pair = ccpairs[ i ]; + + char outline[ 512 ]; + Q_snprintf( outline, sizeof( outline ), "%s\n", pair.fullpath.word ); + + g_pFileSystem->Write( outline, Q_strlen(outline), fh ); + } + + g_pFileSystem->Close( fh ); + } + } +} + +// link all sounds in sentence, start playing first word. +// return number of words loaded +void VOX_TouchSound( const char *pszin, CUtlDict< int, int >& filelist, CUtlRBTree< ccpair, int >& ccpairs, bool spewsentences ) +{ +#ifndef SWDS + char buffer[512]; + int i, cword; + char pathbuffer[MAX_PATH]; + char szpath[MAX_PATH]; + voxword_t rgvoxword[CVOXWORDMAX]; + char *psz; + + if (!pszin) + return; + + memset(rgvoxword, 0, sizeof (voxword_t) * CVOXWORDMAX); + memset(buffer, 0, sizeof(buffer)); + + // lookup actual string in g_Sentences, + // set pointer to string data + + psz = VOX_LookupString(pszin, NULL); + + if (!psz) + { + DevMsg ("VOX_TouchSound: no sentence named %s\n",pszin); + return; + } + + // get directory from string, advance psz + psz = VOX_GetDirectory(szpath, sizeof( szpath ), psz ); + + if ( Q_strlen(psz) > sizeof(buffer) - 1 ) + { + DevMsg ("VOX_TouchSound: sentence is too long %s\n",psz); + return; + } + + // copy into buffer + Q_strncpy(buffer, psz, sizeof( buffer ) ); + psz = buffer; + + // parse sentence (also inserts null terminators between words) + + VOX_ParseString(psz); + + // for each word in the sentence, construct the filename, + // lookup the sfx and save each pointer in a temp array + + i = 0; + cword = 0; + + CUtlVector< WordBuf > rep; + + while (rgpparseword[i]) + { + // Get any pitch, volume, start, end params into voxword + + if ( VOX_ParseWordParams(rgpparseword[i], &rgvoxword[cword], i == 0 ) ) + { + // Iterate all virtuals here... + if ( !Q_strnicmp( rgpparseword[i], "V_", 2 ) ) + { + CUtlVector< WordBuf > list; + + VOX_BuildVirtualNameList( rgpparseword[i], list ); + + int c = list.Count(); + for ( int j = 0 ; j < c; ++j ) + { + char name[ 256 ]; + Q_snprintf( name, sizeof( name ), "%s", list[ j ].word ); + + if ( !Q_strnicmp( name, "V_", 2 ) ) + { + Warning( "VOX_TouchSound didn't resolve virtual token %s!\n", name ); + } + + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, name ); + VOX_Touch( pathbuffer, filelist ); + + WordBuf w; + if ( j == 0 ) + { + w.Set( name ); + rep.AddToTail( w ); + } + ccpair pair; + Q_snprintf( pair.token.word, sizeof( pair.token.word ), "S(%s%s)", szpath, name ); + pair.value.Set( name ); + + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s/sound/%s%s.wav", g_pSoundServices->GetGameDir(), szpath, name ); + Q_FixSlashes( pathbuffer, '\\' ); + pair.fullpath.Set( pathbuffer ); + + if ( ccpairs.Find( pair ) == ccpairs.InvalidIndex() ) + { + ccpairs.Insert( pair ); + } + } + } + else + { + // this is a valid word (as opposed to a parameter block) + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s%s.wav", szpath, rgpparseword[i] ); + VOX_Touch( pathbuffer, filelist ); + + WordBuf w; + w.Set( rgpparseword[ i ] ); + rep.AddToTail( w ); + + ccpair pair; + Q_snprintf( pair.token.word, sizeof( pair.token.word ), "S(%s%s)", szpath, rgpparseword[i] ); + pair.value.Set( rgpparseword[i] ); + + Q_snprintf( pathbuffer, sizeof( pathbuffer ), "%s/sound/%s%s.wav", g_pSoundServices->GetGameDir(), szpath, rgpparseword[ i ] ); + Q_FixSlashes( pathbuffer, CORRECT_PATH_SEPARATOR ); + pair.fullpath.Set( pathbuffer ); + + if ( ccpairs.Find( pair ) == ccpairs.InvalidIndex() ) + { + ccpairs.Insert( pair ); + } + } + } + i++; + } + + if ( spewsentences ) + { + char outbuf[ 1024 ]; + // Build representative text + outbuf[ 0 ] = 0; + for ( i = 0; i < rep.Count(); ++i ) + { + /* + if ( !Q_stricmp( rep[ i ].word, "_comma" ) ) + { + if ( i != 0 && Q_strlen( outbuf ) >= 1 ) + { + outbuf[ Q_strlen( outbuf ) - 1 ] =0; + } + + // Don't end sentence with comma.. + if ( i != rep.Count() - 1 ) + { + Q_strncat( outbuf, ", ", sizeof( outbuf ), COPY_ALL_CHARACTERS ); + } + continue; + } + */ + + Q_strncat( outbuf, rep[ i ].word, sizeof( outbuf ), COPY_ALL_CHARACTERS ); + if ( i != rep.Count() - 1 ) + { + Q_strncat( outbuf, " ", sizeof( outbuf ), COPY_ALL_CHARACTERS ); + } + } + + Msg( " %s\n", outbuf ); + } +#endif +} + + +//----------------------------------------------------------------------------- +// Purpose: Take a NULL terminated sentence, and parse any commands contained in +// {}. The string is rewritten in place with those commands removed. +// +// Input : *pSentenceData - sentence data to be modified in place +// sentenceIndex - global sentence table index for any data that is +// parsed out +//----------------------------------------------------------------------------- +void VOX_ParseLineCommands( char *pSentenceData, int sentenceIndex ) +{ + char tempBuffer[512]; + char *pNext, *pStart; + int length, tempBufferPos = 0; + + if ( !pSentenceData ) + return; + + pStart = pSentenceData; + + while ( *pSentenceData ) + { + pNext = ScanForwardUntil( pSentenceData, '{' ); + + // Find length of "good" portion of the string (not a {} command) + length = pNext - pSentenceData; + if ( tempBufferPos + length > sizeof(tempBuffer) ) + { + DevMsg("Error! sentence too long!\n" ); + return; + } + + // Copy good string to temp buffer + memcpy( tempBuffer + tempBufferPos, pSentenceData, length ); + + // Move the copy position + tempBufferPos += length; + + pSentenceData = pNext; + + // Skip ahead of the opening brace + if ( *pSentenceData ) + { + pSentenceData++; + } + + while ( 1 ) + { + // Skip whitespace + while ( *pSentenceData && *pSentenceData <= 32 ) + { + pSentenceData++; + } + + // Simple comparison of string commands: + switch( tolower( *pSentenceData ) ) + { + case 'l': + // All commands starting with the letter 'l' here + if ( !Q_strnicmp( pSentenceData, "len", 3 ) ) + { + g_Sentences[sentenceIndex].length = atof( pSentenceData + 3 ) ; + + // "len " len + space + pSentenceData += 4; + + // Skip until next } or whitespace character + while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) + pSentenceData++; + } + break; + case 'c': + // This sentence should emit a close caption + if ( !Q_strnicmp( pSentenceData, "closecaption", 12 ) ) + { + g_Sentences[sentenceIndex].closecaption = true; + + pSentenceData += 12; + + pSentenceData = (char *)COM_Parse( pSentenceData ); + + // Skip until next } or whitespace character + while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) + pSentenceData++; + + if ( Q_strlen( com_token ) > 0 ) + { + g_Sentences[sentenceIndex].caption = com_token; + } + else + { + g_Sentences[sentenceIndex].caption = UTL_INVAL_SYMBOL; + } + } + break; + case 0: + default: + { + // Skip until next } or whitespace character + while ( *pSentenceData && ( *pSentenceData != '}' && !( *pSentenceData <= 32 ) ) ) + pSentenceData++; + } + break; + } + + // Done? + if ( !*pSentenceData || *pSentenceData == '}' ) + { + break; + } + } + + // pSentenceData = ScanForwardUntil( pSentenceData, '}' ); + + // Skip the closing brace + if ( *pSentenceData ) + pSentenceData++; + + // Skip trailing whitespace + while ( *pSentenceData && *pSentenceData <= 32 ) + pSentenceData++; + } + + if ( tempBufferPos < sizeof(tempBuffer) ) + { + // terminate cleaned up copy + tempBuffer[ tempBufferPos ] = 0; + + // Copy it over the original data + Q_strcpy( pStart, tempBuffer ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Add a new group or increment count of the existing one +// Input : *pSentenceName - text of the sentence name +//----------------------------------------------------------------------------- +int VOX_GroupAdd( const char *pSentenceName ) +{ + int len = strlen( pSentenceName ) - 1; + + // group members end in a number + if ( len <= 0 || !V_isdigit(pSentenceName[len]) ) + return -1; + + // truncate away the index + while ( len > 0 && V_isdigit(pSentenceName[len]) ) + { + len--; + } + + // make a copy of the actual group name + char *groupName = (char *)stackalloc( len + 2 ); + Q_strncpy( groupName, pSentenceName, len+2 ); + + // check for it in the list + int i; + sentencegroup_t *pGroup; + + CUtlSymbol symGroupName = sentencegroup_t::GetSymbol( groupName ); + int groupCount = g_SentenceGroups.Size(); + for ( i = 0; i < groupCount; i++ ) + { + int groupIndex = (i + groupCount-1) % groupCount; + + // Start at the last group a loop around + pGroup = &g_SentenceGroups[groupIndex]; + if ( symGroupName == pGroup->GroupNameSymbol() ) + { + // Matches previous group, bump count + pGroup->count++; + return i; + } + } + + // new group + int addIndex = g_SentenceGroups.AddToTail(); + sentencegroup_t *group = &g_SentenceGroups[addIndex]; + group->SetGroupName( groupName ); + group->count = 1; + return addIndex; +} + +#if DEAD +//----------------------------------------------------------------------------- +// Purpose: clear the sentence groups +//----------------------------------------------------------------------------- +void VOX_GroupClear( void ) +{ + g_SentenceGroups.RemoveAll(); +} +#endif + + +void VOX_LRUInit( sentencegroup_t *pGroup ) +{ + int i, n1, n2, temp; + + if ( pGroup->count ) + { + unsigned char *pLRU = &g_GroupLRU[pGroup->lru]; + for (i = 0; i < pGroup->count; i++) + pLRU[i] = (unsigned char) i; + + // randomize array by swapping random elements + for (i = 0; i < (pGroup->count * 4); i++) + { + // FIXME: This should probably call through g_pSoundServices + // or some other such call? + n1 = RandomInt(0,pGroup->count-1); + n2 = RandomInt(0,pGroup->count-1); + temp = pLRU[n1]; + pLRU[n1] = pLRU[n2]; + pLRU[n2] = temp; + } + } +} + + +//----------------------------------------------------------------------------- +// Purpose: Init the LRU for each sentence group +//----------------------------------------------------------------------------- +void VOX_GroupInitAllLRUs( void ) +{ + int i; + + int totalCount = 0; + for ( i = 0; i < g_SentenceGroups.Size(); i++ ) + { + g_SentenceGroups[i].lru = totalCount; + totalCount += g_SentenceGroups[i].count; + } + g_GroupLRU.Purge(); + g_GroupLRU.EnsureCount( totalCount ); + for ( i = 0; i < g_SentenceGroups.Size(); i++ ) + { + VOX_LRUInit( &g_SentenceGroups[i] ); + } +} + +//----------------------------------------------------------------------------- +// Purpose: Only during reslist generation +//----------------------------------------------------------------------------- +void VOX_AddSentenceWavesToResList( void ) +{ + if ( !CommandLine()->FindParm( "-makereslists" ) && + !CommandLine()->FindParm( "-spewsentences" ) ) + { + return; + } + + bool spewsentences = CommandLine()->FindParm( "-spewsentences" ) != 0 ? true : false; + + CUtlDict< int, int > list; + CUtlRBTree< ccpair, int > ccpairs( 0, 0, CCPairLessFunc ); + + int i; + int sentencecount = g_Sentences.Count(); + + for ( i = 0; i < sentencecount; i++ ) + { + // Walk through all nonvirtual sentences and touch the referenced sounds... + sentence_t *pSentence = &g_Sentences[i]; + + if ( !Q_strnicmp( pSentence->pName, "V_", 2 ) ) + { + continue; + } + + if ( spewsentences ) + { + const char *psz = VOX_LookupString(pSentence->pName, NULL); + if ( psz ) + { + Msg( "%s : %s\n", pSentence->pName, psz ); + } + } + + VOX_TouchSound( pSentence->pName, list, ccpairs, spewsentences ); + + } + + VOX_TouchSounds( list, ccpairs, spewsentences ); + + list.RemoveAll(); +} + + +//----------------------------------------------------------------------------- +// Purpose: Given a group name, return that group's index +// Input : *pGroupName - name of the group +// Output : int - index in group table, returns -1 if no matching group is found +//----------------------------------------------------------------------------- +int VOX_GroupIndexFromName( const char *pGroupName ) +{ + int i; + + if ( pGroupName ) + { + // search rgsentenceg for match on szgroupname + CUtlSymbol symGroupName = sentencegroup_t::GetSymbol( pGroupName ); + for ( i = 0; i < g_SentenceGroups.Size(); i++ ) + { + if ( symGroupName == g_SentenceGroups[i].GroupNameSymbol() ) + return i; + } + } + + return -1; +} + + +//----------------------------------------------------------------------------- +// Purpose: return the group's name +// Input : groupIndex - index of the group +// Output : const char * - name pointer +//----------------------------------------------------------------------------- +const char *VOX_GroupNameFromIndex( int groupIndex ) +{ + if ( groupIndex >= 0 && groupIndex < g_SentenceGroups.Size() ) + return g_SentenceGroups[groupIndex].GroupName(); + + return NULL; +} + +// ignore lru. pick next sentence from sentence group. Go in order until we hit the last sentence, +// then repeat list if freset is true. If freset is false, then repeat last sentence. +// ipick is passed in as the requested sentence ordinal. +// ipick 'next' is returned. +// return of -1 indicates an error. + +int VOX_GroupPickSequential( int isentenceg, char *szfound, int szfoundLen, int ipick, int freset ) +{ + const char *szgroupname; + unsigned char count; + + if (isentenceg < 0 || isentenceg > g_SentenceGroups.Size()) + return -1; + + szgroupname = g_SentenceGroups[isentenceg].GroupName(); + count = g_SentenceGroups[isentenceg].count; + + if (count == 0) + return -1; + + if (ipick >= count) + ipick = count-1; + + Q_snprintf( szfound, szfoundLen, "!%s%d", szgroupname, ipick ); + + if (ipick >= count) + { + if (freset) + // reset at end of list + return 0; + else + return count; + } + + return ipick + 1; +} + + + +// pick a random sentence from rootname0 to rootnameX. +// picks from the rgsentenceg[isentenceg] least +// recently used, modifies lru array. returns the sentencename. +// note, lru must be seeded with 0-n randomized sentence numbers, with the +// rest of the lru filled with -1. The first integer in the lru is +// actually the size of the list. Returns ipick, the ordinal +// of the picked sentence within the group. + +int VOX_GroupPick( int isentenceg, char *szfound, int strLen ) +{ + const char *szgroupname; + unsigned char *plru; + unsigned char i; + unsigned char count; + unsigned char ipick=0; + int ffound = FALSE; + + if (isentenceg < 0 || isentenceg > g_SentenceGroups.Size()) + return -1; + + szgroupname = g_SentenceGroups[isentenceg].GroupName(); + count = g_SentenceGroups[isentenceg].count; + plru = &g_GroupLRU[g_SentenceGroups[isentenceg].lru]; + + while (!ffound) + { + for (i = 0; i < count; i++) + if (plru[i] != 0xFF) + { + ipick = plru[i]; + plru[i] = 0xFF; + ffound = TRUE; + break; + } + + if (!ffound) + { + VOX_LRUInit( &g_SentenceGroups[isentenceg] ); + } + else + { + Q_snprintf( szfound, strLen, "!%s%d", szgroupname, ipick ); + return ipick; + } + } + return -1; +} + + +struct filelist_t +{ + const char *pFileName; + filelist_t *pNext; +}; + +static filelist_t *g_pSentenceFileList = NULL; + +//----------------------------------------------------------------------------- +// Purpose: clear / reinitialize the vox list +//----------------------------------------------------------------------------- +void VOX_ListClear( void ) +{ + filelist_t *pList, *pNext; + + pList = g_pSentenceFileList; + + while ( pList ) + { + pNext = pList->pNext; + free( pList ); + + pList = pNext; + } + + g_pSentenceFileList = NULL; +} + +//----------------------------------------------------------------------------- +// Purpose: Check to see if this file is in the list +// Input : *psentenceFileName - +// Output : int, true if the file is in the list, false if not +//----------------------------------------------------------------------------- +int VOX_ListFileIsLoaded( const char *psentenceFileName ) +{ + filelist_t *pList = g_pSentenceFileList; + while ( pList ) + { + if ( !strcmp( psentenceFileName, pList->pFileName ) ) + return true; + + pList = pList->pNext; + } + + return false; +} + +//----------------------------------------------------------------------------- +// Purpose: Add this file name to the sentence list +// Input : *psentenceFileName - +//----------------------------------------------------------------------------- +void VOX_ListMarkFileLoaded( const char *psentenceFileName ) +{ + filelist_t *pEntry; + char *pName; + + pEntry = (filelist_t *)malloc( sizeof(filelist_t) + strlen( psentenceFileName ) + 1); + + if ( pEntry ) + { + pName = (char *)(pEntry+1); + Q_strcpy( pName, psentenceFileName ); + + pEntry->pFileName = pName; + pEntry->pNext = g_pSentenceFileList; + + g_pSentenceFileList = pEntry; + } +} + +// This creates a compact copy of the sentence file in memory with only the necessary data +void VOX_CompactSentenceFile() +{ + int totalMem = 0; + int i; + for ( i = 0; i < g_Sentences.Count(); i++ ) + { + int len = Q_strlen( g_Sentences[i].pName ) + 1; + const char *pData = g_Sentences[i].pName + len; + int dataLen = Q_strlen( pData ) + 1; + totalMem += len + dataLen; + } + g_SentenceFile.EnsureCount( totalMem ); + totalMem = 0; + for ( i = 0; i < g_Sentences.Count(); i++ ) + { + int len = Q_strlen( g_Sentences[i].pName ) + 1; + const char *pData = g_Sentences[i].pName + len; + int dataLen = Q_strlen( pData ) + 1; + char *pDest = &g_SentenceFile[totalMem]; + memcpy( pDest, g_Sentences[i].pName, len + dataLen ); + g_Sentences[i].pName = pDest; + totalMem += len + dataLen; + } +} + +// Load sentence file into memory, insert null terminators to +// delimit sentence name/sentence pairs. Keep pointer to each +// sentence name so we can search later. + +void VOX_ReadSentenceFile( const char *psentenceFileName ) +{ + char *pch; + byte *pFileData; + int fileSize; + char c; + char *pchlast, *pSentenceData; + characterset_t whitespace; + + // Have we already loaded this file? + if ( VOX_ListFileIsLoaded( psentenceFileName ) ) + { + // must touch any sentence wavs again to ensure the map's init path gets the results + if ( MapReslistGenerator().IsLoggingToMap() ) + { + VOX_AddSentenceWavesToResList(); + } + return; + } + + // load file + + FileHandle_t file; + file = g_pFileSystem->Open( psentenceFileName, "rb" ); + if ( FILESYSTEM_INVALID_HANDLE == file ) + { + DevMsg ("Couldn't load %s\n", psentenceFileName); + return; + } + + fileSize = g_pFileSystem->Size( file ); + if ( fileSize <= 0 ) + { + DevMsg ("VOX_ReadSentenceFile: %s has invalid size %i\n", psentenceFileName, fileSize ); + g_pFileSystem->Close( file ); + return; + } + + pFileData = (byte *)g_pFileSystem->AllocOptimalReadBuffer( file, fileSize + 1 ); + if ( !pFileData ) + { + DevMsg ("VOX_ReadSentenceFile: %s couldn't allocate %i bytes for data\n", psentenceFileName, fileSize ); + g_pFileSystem->Close( file ); + return; + } + + // Read the data and close the file + g_pFileSystem->ReadEx( pFileData, g_pFileSystem->GetOptimalReadSize( file, fileSize ), fileSize, file ); + g_pFileSystem->Close( file ); + + // Make sure we end with a null terminator + pFileData[ fileSize ] = 0; + + pch = (char *)pFileData; + pchlast = pch + fileSize; + CharacterSetBuild( &whitespace, "\n\r\t " ); + const char *pName = 0; + while (pch < pchlast) + { + // Only process this pass on sentences + pSentenceData = NULL; + + // skip newline, cr, tab, space + + c = *pch; + while (pch < pchlast && IN_CHARACTERSET( whitespace, c )) + c = *(++pch); + + // YWB: Fix possible crashes reading past end of file if the last line has only whitespace on it... + if ( !*pch ) + break; + + // skip entire line if first char is / + if (*pch != '/') + { + int addIndex = g_Sentences.AddToTail(); + sentence_t *pSentence = &g_Sentences[addIndex]; + pName = pch; + pSentence->pName = pch; + pSentence->length = 0; + pSentence->closecaption = false; + pSentence->isPrecached = false; + pSentence->caption = UTL_INVAL_SYMBOL; + + // scan forward to first space, insert null terminator + // after sentence name + + c = *pch; + while (pch < pchlast && c != ' ') + c = *(++pch); + + if (pch < pchlast) + *pch++ = 0; + + // A sentence may have some line commands, make an extra pass + pSentenceData = pch; + } + // scan forward to end of sentence or eof + while (pch < pchlast && pch[0] != '\n' && pch[0] != '\r') + pch++; + + // insert null terminator + if (pch < pchlast) + *pch++ = 0; + + // If we have some sentence data, parse out any line commands + if ( pSentenceData && pSentenceData < pchlast ) + { + // Add a new group or increment count of the existing one + VOX_GroupAdd( pName ); + int index = g_Sentences.Size()-1; + // The current sentence has an index of count-1 + VOX_ParseLineCommands( pSentenceData, index ); + + } + } + // now compact the file data in memory + VOX_CompactSentenceFile(); + g_pFileSystem->FreeOptimalReadBuffer( pFileData ); + + VOX_GroupInitAllLRUs(); + + // This only does stuff during reslist generation... + VOX_AddSentenceWavesToResList(); + + VOX_ListMarkFileLoaded( psentenceFileName ); +} + + +//----------------------------------------------------------------------------- +// Purpose: Get the current number of sentences in the database +// Output : int +//----------------------------------------------------------------------------- +int VOX_SentenceCount( void ) +{ + return g_Sentences.Size(); +} + + +float VOX_SentenceLength( int sentence_num ) +{ + if ( sentence_num < 0 || sentence_num > g_Sentences.Size()-1 ) + return 0.0f; + + return g_Sentences[ sentence_num ].length; +} + +// scan g_Sentences, looking for pszin sentence name +// return pointer to sentence data if found, null if not +// CONSIDER: if we have a large number of sentences, should +// CONSIDER: sort strings in g_Sentences and do binary search. +char *VOX_LookupString(const char *pSentenceName, int *psentencenum, bool *pbEmitCaption /*=NULL*/, CUtlSymbol *pCaptionSymbol /*=NULL*/, float *pflDuration /*= NULL*/ ) +{ + if ( pbEmitCaption ) + { + *pbEmitCaption = false; + } + + if ( pCaptionSymbol ) + { + *pCaptionSymbol = UTL_INVAL_SYMBOL; + } + + if ( pflDuration ) + { + *pflDuration = 0.0f; + } + + int i; + int c = g_Sentences.Size(); + for (i = 0; i < c; i++) + { + char const *name = g_Sentences[i].pName; + + if (!stricmp(pSentenceName, name)) + { + if (psentencenum) + { + *psentencenum = i; + } + + if ( pbEmitCaption ) + { + *pbEmitCaption = g_Sentences[ i ].closecaption; + } + + if ( pCaptionSymbol ) + { + *pCaptionSymbol = g_Sentences[ i ].caption; + } + + if ( pflDuration ) + { + *pflDuration = g_Sentences[ i ].length; + } + + return (char *)(name + Q_strlen(name) + 1); + } + } + return NULL; +} + + +// Abstraction for sentence name array +const char *VOX_SentenceNameFromIndex( int sentencenum ) +{ + if ( sentencenum < g_Sentences.Size() ) + return g_Sentences[sentencenum].pName; + return NULL; +} + + + diff --git a/engine/audio/private/vox_private.h b/engine/audio/private/vox_private.h new file mode 100644 index 0000000..cd29c05 --- /dev/null +++ b/engine/audio/private/vox_private.h @@ -0,0 +1,74 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOX_PRIVATE_H +#define VOX_PRIVATE_H +#pragma once + +#ifndef VOX_H +#include "vox.h" +#endif + +#ifndef UTLVECTOR_H +#include "utlvector.h" +#endif + +#include "utlsymbol.h" + +struct channel_t; +class CSfxTable; +class CAudioMixer; + +struct voxword_t +{ + int volume; // increase percent, ie: 125 = 125% increase + int pitch; // pitch shift up percent + int start; // offset start of wave percent + int end; // offset end of wave percent + int cbtrim; // end of wave after being trimmed to 'end' + int fKeepCached; // 1 if this word was already in cache before sentence referenced it + int samplefrac; // if pitch shifting, this is position into wav * 256 + int timecompress; // % of wave to skip during playback (causes no pitch shift) + CSfxTable *sfx; // name and cache pointer +}; + +#define CVOXWORDMAX 32 +#define CVOXZEROSCANMAX 255 // scan up to this many samples for next zero crossing + +struct sentence_t +{ + sentence_t() : + pName( 0 ), + length( 0.0f ), + closecaption( false ) + { + } + + char *pName; + float length; + bool closecaption : 7; + bool isPrecached : 1; + CUtlSymbol caption; +}; + +extern CUtlVector<sentence_t> g_Sentences; + + +extern int VOX_FPaintPitchChannelFrom8( channel_t *ch, sfxcache_t *sc, int count, int pitch, int timecompress ); +extern void VOX_TrimStartEndTimes( channel_t *ch, sfxcache_t *sc ); +extern int VOX_ParseWordParams( char *psz, voxword_t *pvoxword, int fFirst ); +extern void VOX_SetChanVol( channel_t *ch ); +extern char **VOX_ParseString( char *psz ); +extern CAudioMixer *CreateSentenceMixer( voxword_t *pwords ); + +#endif // VOX_PRIVATE_H diff --git a/engine/audio/public/ivoicecodec.h b/engine/audio/public/ivoicecodec.h new file mode 100644 index 0000000..f7c43b3 --- /dev/null +++ b/engine/audio/public/ivoicecodec.h @@ -0,0 +1,61 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Define the IVoiceCodec interface. +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef IVOICECODEC_H +#define IVOICECODEC_H +#pragma once + + +#include "interface.h" + + +#define BYTES_PER_SAMPLE 2 + + +// This interface is for voice codecs to implement. + +// Codecs are guaranteed to be called with the exact output from Compress into Decompress (ie: +// data won't be stuck together and sent to Decompress). + +// Decompress is not guaranteed to be called in any specific order relative to Compress, but +// Codecs maintain state between calls, so it is best to call Compress with consecutive voice data +// and decompress likewise. If you call it out of order, it will sound wierd. + +// In the same vein, calling Decompress twice with the same data is a bad idea since the state will be +// expecting the next block of data, not the same block. + +class IVoiceCodec +{ +protected: + virtual ~IVoiceCodec() {} + +public: + // Initialize the object. The uncompressed format is always 8-bit signed mono. + virtual bool Init( int quality )=0; + + // Use this to delete the object. + virtual void Release()=0; + + + // Compress the voice data. + // pUncompressed - 16-bit signed mono voice data. + // maxCompressedBytes - The length of the pCompressed buffer. Don't exceed this. + // bFinal - Set to true on the last call to Compress (the user stopped talking). + // Some codecs like big block sizes and will hang onto data you give them in Compress calls. + // When you call with bFinal, the codec will give you compressed data no matter what. + // Return the number of bytes you filled into pCompressed. + virtual int Compress(const char *pUncompressed, int nSamples, char *pCompressed, int maxCompressedBytes, bool bFinal)=0; + + // Decompress voice data. pUncompressed is 16-bit signed mono. + virtual int Decompress(const char *pCompressed, int compressedBytes, char *pUncompressed, int maxUncompressedBytes)=0; + + // Some codecs maintain state between Compress and Decompress calls. This should clear that state. + virtual bool ResetState()=0; +}; + + +#endif // IVOICECODEC_H diff --git a/engine/audio/public/ivoicerecord.h b/engine/audio/public/ivoicerecord.h new file mode 100644 index 0000000..9f830ff --- /dev/null +++ b/engine/audio/public/ivoicerecord.h @@ -0,0 +1,40 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef IVOICERECORD_H +#define IVOICERECORD_H +#pragma once + + +// This is the voice recording interface. It provides 16-bit signed mono data from +// a mic at some sample rate. +abstract_class IVoiceRecord +{ +protected: + + virtual ~IVoiceRecord() {} + + +public: + + // Use this to delete the object. + virtual void Release()=0; + + // Start/stop capturing. + virtual bool RecordStart() = 0; + virtual void RecordStop() = 0; + + // Idle processing. + virtual void Idle()=0; + + // Get the most recent N samples. If nSamplesWanted is less than the number of + // available samples, it discards the first samples and gives you the last ones. + virtual int GetRecordedData(short *pOut, int nSamplesWanted)=0; +}; + + +#endif // IVOICERECORD_H diff --git a/engine/audio/public/snd_audio_source.h b/engine/audio/public/snd_audio_source.h new file mode 100644 index 0000000..b8ac6b9 --- /dev/null +++ b/engine/audio/public/snd_audio_source.h @@ -0,0 +1,526 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_AUDIO_SOURCE_H +#define SND_AUDIO_SOURCE_H +#pragma once + +#if !defined( _X360 ) +#define MP3_SUPPORT 1 +#endif + +#define AUDIOSOURCE_COPYBUF_SIZE 4096 + +struct channel_t; +class CSentence; +class CSfxTable; + +class CAudioSource; +class IAudioDevice; +class CUtlBuffer; + +#include "tier0/vprof.h" + +//----------------------------------------------------------------------------- +// Purpose: This is an instance of an audio source. +// Mixers are attached to channels and reference an audio source. +// Mixers are specific to the sample format and source format. +// Mixers are never re-used, so they can track instance data like +// sample position, fractional sample, stream cache, faders, etc. +//----------------------------------------------------------------------------- +abstract_class CAudioMixer +{ +public: + virtual ~CAudioMixer( void ) {} + + // return number of samples mixed + virtual int MixDataToDevice( IAudioDevice *pDevice, channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) = 0; + virtual int SkipSamples( channel_t *pChannel, int sampleCount, int outputRate, int outputOffset ) = 0; + virtual bool ShouldContinueMixing( void ) = 0; + + virtual CAudioSource *GetSource( void ) = 0; + + // get the current position (next sample to be mixed) + virtual int GetSamplePosition( void ) = 0; + + // Allow the mixer to modulate pitch and volume. + // returns a floating point modulator + virtual float ModifyPitch( float pitch ) = 0; + virtual float GetVolumeScale( void ) = 0; + + // NOTE: Playback is optimized for linear streaming. These calls will usually cost performance + // It is currently optimal to call them before any playback starts, but some audio sources may not + // guarantee this. Also, some mixers may choose to ignore these calls for internal reasons (none do currently). + + // Move the current position to newPosition + // BUGBUG: THIS CALL DOES NOT SUPPORT MOVING BACKWARD, ONLY FORWARD!!! + virtual void SetSampleStart( int newPosition ) = 0; + + // End playback at newEndPosition + virtual void SetSampleEnd( int newEndPosition ) = 0; + + // How many samples to skip before commencing actual data reading ( to allow sub-frametime sound + // offsets and avoid synchronizing sounds to various 100 msec clock intervals throughout the + // engine and game code) + virtual void SetStartupDelaySamples( int delaySamples ) = 0; + virtual int GetMixSampleSize() = 0; + + // Certain async loaded sounds lazilly load into memory in the background, use this to determine + // if the sound is ready for mixing + virtual bool IsReadyToMix() = 0; + + // NOTE: The "saved" position can be different than the "sample" position + // NOTE: Allows mixer to save file offsets, loop info, etc + virtual int GetPositionForSave() = 0; + virtual void SetPositionFromSaved( int savedPosition ) = 0; +}; + +inline int CalcSampleSize( int bitsPerSample, int _channels ) +{ + return (bitsPerSample >> 3) * _channels; +} + +#include "UtlCachedFileData.h" + +class CSentence; +class CSfxTable; +class CAudioSourceCachedInfo : public IBaseCacheInfo +{ +public: + CAudioSourceCachedInfo(); + CAudioSourceCachedInfo( const CAudioSourceCachedInfo& src ); + + virtual ~CAudioSourceCachedInfo(); + + CAudioSourceCachedInfo& operator =( const CAudioSourceCachedInfo& src ); + + void Clear(); + void RemoveData(); + + virtual void Save( CUtlBuffer& buf ); + virtual void Restore( CUtlBuffer& buf ); + virtual void Rebuild( char const *filename ); + + // A hack, but will work okay + static int s_CurrentType; + static CSfxTable *s_pSfx; + static bool s_bIsPrecacheSound; + + inline int Type() const + { + return info.m_Type; + } + void SetType( int type ) + { + info.m_Type = type; + } + + inline int Bits() const + { + return info.m_bits; + } + void SetBits( int bits ) + { + info.m_bits = bits; + } + + inline int Channels() const + { + return info.m_channels; + } + void SetChannels( int _channels ) + { + info.m_channels = _channels; + } + + inline int SampleSize() const + { + return info.m_sampleSize; + } + void SetSampleSize( int size ) + { + info.m_sampleSize = size; + } + + inline int Format() const + { + return info.m_format; + } + void SetFormat( int format ) + { + info.m_format = format; + } + + inline int SampleRate() const + { + return info.m_rate; + } + void SetSampleRate( int rate ) + { + info.m_rate = rate; + } + + inline int CachedDataSize() const + { + return (int)m_usCachedDataSize; + } + + void SetCachedDataSize( int size ) + { + m_usCachedDataSize = (unsigned short)size; + } + + inline const byte *CachedData() const + { + return m_pCachedData; + } + + void SetCachedData( const byte *data ) + { + m_pCachedData = ( byte * )data; + flags.m_bCachedData = ( data != NULL ) ? true : false; + } + + inline int HeaderSize() const + { + return (int)m_usHeaderSize; + } + + void SetHeaderSize( int size ) + { + m_usHeaderSize = (unsigned short)size; + } + + inline const byte *HeaderData() const + { + return m_pHeader; + } + + void SetHeaderData( const byte *data ) + { + m_pHeader = ( byte * )data; + flags.m_bHeader = ( data != NULL ) ? true : false; + } + + inline int LoopStart() const + { + return m_loopStart; + } + void SetLoopStart( int start ) + { + m_loopStart = start; + } + + inline int SampleCount() const + { + return m_sampleCount; + } + + void SetSampleCount( int count ) + { + m_sampleCount = count; + } + inline int DataStart() const + { + return m_dataStart; + } + void SetDataStart( int start ) + { + m_dataStart = start; + } + inline int DataSize() const + { + return m_dataSize; + } + void SetDataSize( int size ) + { + m_dataSize = size; + } + inline CSentence *Sentence() const + { + return m_pSentence; + } + void SetSentence( CSentence *sentence ) + { + m_pSentence = sentence; + flags.m_bSentence = ( sentence != NULL ) ? true : false; + } + +private: + + union + { + unsigned int infolong; + struct + { + unsigned int m_Type : 2; // 0 1 2 or 3 + unsigned int m_bits : 5; // 0 to 31 + unsigned int m_channels : 2; // 1 or 2 + unsigned int m_sampleSize : 3; // 1 2 or 4 + unsigned int m_format : 2; // 1 == PCM, 2 == ADPCM + unsigned int m_rate : 17; // 0 to 64 K + } info; + }; + + union + { + byte flagsbyte; + struct + { + bool m_bSentence : 1; + bool m_bCachedData : 1; + bool m_bHeader : 1; + } flags; + }; + + int m_loopStart; + int m_sampleCount; + int m_dataStart; // offset of wave data chunk + int m_dataSize; // size of wave data chunk + + unsigned short m_usCachedDataSize; + unsigned short m_usHeaderSize; + + CSentence *m_pSentence; + byte *m_pCachedData; + byte *m_pHeader; +}; + +class IAudioSourceCache +{ +public: + virtual bool Init( unsigned int memSize ) = 0; + virtual void Shutdown() = 0; + virtual void LevelInit( char const *mapname ) = 0; + virtual void LevelShutdown() = 0; + + // This invalidates the cached size/date info for sounds so it'll regenerate that next time it's accessed. + // Used when you connect to a pure server. + virtual void ForceRecheckDiskInfo() = 0; + + virtual CAudioSourceCachedInfo *GetInfo( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ) = 0; + virtual void RebuildCacheEntry( int audiosourcetype, bool soundisprecached, CSfxTable *sfx ) = 0; +}; + +extern IAudioSourceCache *audiosourcecache; + +FORWARD_DECLARE_HANDLE( memhandle_t ); + +typedef int StreamHandle_t; +enum +{ + INVALID_STREAM_HANDLE = (StreamHandle_t)~0 +}; + +typedef int BufferHandle_t; +enum +{ + INVALID_BUFFER_HANDLE = (BufferHandle_t)~0 +}; + +typedef unsigned int streamFlags_t; +enum +{ + STREAMED_FROMDVD = 0x00000001, // stream buffers are compliant to dvd sectors + STREAMED_SINGLEPLAY = 0x00000002, // non recurring data, buffers don't need to persist and can be recycled + STREAMED_QUEUEDLOAD = 0x00000004, // hint the streamer to load using the queued loader system +}; + +abstract_class IAsyncWavDataCache +{ +public: + virtual bool Init( unsigned int memSize ) = 0; + virtual void Shutdown() = 0; + + // implementation that treats file as monolithic + virtual memhandle_t AsyncLoadCache( char const *filename, int datasize, int startpos, bool bIsPrefetch = false ) = 0; + virtual void PrefetchCache( char const *filename, int datasize, int startpos ) = 0; + virtual bool CopyDataIntoMemory( char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ) = 0; + virtual bool CopyDataIntoMemory( memhandle_t& handle, char const *filename, int datasize, int startpos, void *buffer, int bufsize, int copystartpos, int bytestocopy, bool *pbPostProcessed ) = 0; + virtual bool IsDataLoadCompleted( memhandle_t handle, bool *pIsValid ) = 0; + virtual void RestartDataLoad( memhandle_t *pHandle, const char *pFilename, int dataSize, int startpos ) = 0; + virtual bool GetDataPointer( memhandle_t& handle, char const *filename, int datasize, int startpos, void **pData, int copystartpos, bool *pbPostProcessed ) = 0; + virtual void SetPostProcessed( memhandle_t handle, bool proc ) = 0; + virtual void Unload( memhandle_t handle ) = 0; + + // alternate multi-buffer streaming implementation + virtual StreamHandle_t OpenStreamedLoad( char const *pFileName, int dataSize, int dataStart, int startPos, int loopPos, int bufferSize, int numBuffers, streamFlags_t flags ) = 0; + virtual void CloseStreamedLoad( StreamHandle_t hStream ) = 0; + virtual int CopyStreamedDataIntoMemory( StreamHandle_t hStream, void *pBuffer, int buffSize, int copyStartPos, int bytesToCopy ) = 0; + virtual bool IsStreamedDataReady( StreamHandle_t hStream ) = 0; + virtual void MarkBufferDiscarded( BufferHandle_t hBuffer ) = 0; + virtual void *GetStreamedDataPointer( StreamHandle_t hStream, bool bSync ) = 0; + virtual bool IsDataLoadInProgress( memhandle_t handle ) = 0; + virtual void Flush() = 0; + virtual void OnMixBegin() = 0; + virtual void OnMixEnd() = 0; +}; + +extern IAsyncWavDataCache *wavedatacache; + +struct CAudioSourceCachedInfoHandle_t +{ + CAudioSourceCachedInfoHandle_t() : + info( NULL ), + m_FlushCount( 0 ) + { + } + + CAudioSourceCachedInfo *info; + unsigned int m_FlushCount; + + inline CAudioSourceCachedInfo *Get( int audiosourcetype, bool soundisprecached, CSfxTable *sfx, int *pcacheddatasize ) + { + VPROF("CAudioSourceCachedInfoHandle_t::Get"); + + if ( m_FlushCount != s_nCurrentFlushCount ) + { + // Reacquire + info = audiosourcecache->GetInfo( audiosourcetype, soundisprecached, sfx ); + + if ( pcacheddatasize ) + { + *pcacheddatasize = info ? info->CachedDataSize() : 0; + } + + // Tag as current + m_FlushCount = s_nCurrentFlushCount; + } + return info; + } + + inline bool IsValid() + { + return !!( m_FlushCount == s_nCurrentFlushCount ); + } + + inline CAudioSourceCachedInfo *FastGet() + { + VPROF("CAudioSourceCachedInfoHandle_t::FastGet"); + + if ( m_FlushCount != s_nCurrentFlushCount ) + { + return NULL; + } + return info; + } + + static void InvalidateCache(); + static unsigned int s_nCurrentFlushCount; +}; + + +//----------------------------------------------------------------------------- +// Purpose: A source is an abstraction for a stream, cached file, or procedural +// source of audio. +//----------------------------------------------------------------------------- +abstract_class CAudioSource +{ +public: + enum + { + AUDIO_SOURCE_UNK = 0, + AUDIO_SOURCE_WAV, + AUDIO_SOURCE_MP3, + AUDIO_SOURCE_VOICE, + + AUDIO_SOURCE_MAXTYPE, + }; + + enum + { + AUDIO_NOT_LOADED = 0, + AUDIO_IS_LOADED = 1, + AUDIO_LOADING = 2, + }; + + virtual ~CAudioSource( void ) {} + + // Create an instance (mixer) of this audio source + virtual CAudioMixer *CreateMixer( int initialStreamPosition = 0 ) = 0; + + // Serialization for caching + virtual int GetType( void ) = 0; + virtual void GetCacheData( CAudioSourceCachedInfo *info ) = 0; + + // Provide samples for the mixer. You can point pData at your own data, or if you prefer to copy the data, + // you can copy it into copyBuf and set pData to copyBuf. + virtual int GetOutputData( void **pData, int samplePosition, int sampleCount, char copyBuf[AUDIOSOURCE_COPYBUF_SIZE] ) = 0; + + virtual int SampleRate( void ) = 0; + + // Returns true if the source is a voice source. + // This affects the voice_overdrive behavior (all sounds get quieter when + // someone is speaking). + virtual bool IsVoiceSource() = 0; + + // Sample size is in bytes. It will not be accurate for compressed audio. This is a best estimate. + // The compressed audio mixers understand this, but in general do not assume that SampleSize() * SampleCount() = filesize + // or even that SampleSize() is 100% accurate due to compression. + virtual int SampleSize( void ) = 0; + + // Total number of samples in this source. NOTE: Some sources are infinite (mic input), they should return + // a count equal to one second of audio at their current rate. + virtual int SampleCount( void ) = 0; + + virtual int Format( void ) = 0; + virtual int DataSize( void ) = 0; + + virtual bool IsLooped( void ) = 0; + virtual bool IsStereoWav( void ) = 0; + virtual bool IsStreaming( void ) = 0; + virtual int GetCacheStatus( void ) = 0; + int IsCached( void ) { return GetCacheStatus() == AUDIO_IS_LOADED ? true : false; } + virtual void CacheLoad( void ) = 0; + virtual void CacheUnload( void ) = 0; + virtual CSentence *GetSentence( void ) = 0; + + // these are used to find good splice/loop points. + // If not implementing these, simply return sample + virtual int ZeroCrossingBefore( int sample ) = 0; + virtual int ZeroCrossingAfter( int sample ) = 0; + + // mixer's references + virtual void ReferenceAdd( CAudioMixer *pMixer ) = 0; + virtual void ReferenceRemove( CAudioMixer *pMixer ) = 0; + + // check reference count, return true if nothing is referencing this + virtual bool CanDelete( void ) = 0; + + virtual void Prefetch() = 0; + + virtual bool IsAsyncLoad() = 0; + + // Make sure our data is rebuilt into the per-level cache + virtual void CheckAudioSourceCache() = 0; + + virtual char const *GetFileName() = 0; + + virtual void SetPlayOnce( bool ) = 0; + virtual bool IsPlayOnce() = 0; + + // Used to identify a word that is part of a sentence mixing operation + virtual void SetSentenceWord( bool bIsWord ) = 0; + virtual bool IsSentenceWord() = 0; + + virtual int SampleToStreamPosition( int samplePosition ) = 0; + virtual int StreamToSamplePosition( int streamPosition ) = 0; +}; + +// Fast method for determining duration of .wav/.mp3, exposed to server as well +extern float AudioSource_GetSoundDuration( char const *pName ); + +// uses wave file cached in memory already +extern float AudioSource_GetSoundDuration( CSfxTable *pSfx ); + +#endif // SND_AUDIO_SOURCE_H diff --git a/engine/audio/public/snd_device.h b/engine/audio/public/snd_device.h new file mode 100644 index 0000000..9c844b6 --- /dev/null +++ b/engine/audio/public/snd_device.h @@ -0,0 +1,127 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: This abstracts the various hardware dependent implementations of sound +// At the time of this writing there are Windows WAVEOUT, Direct Sound, +// and Null implementations. +// +//=====================================================================================// + +#ifndef SND_DEVICE_H +#define SND_DEVICE_H +#pragma once + +#include "snd_fixedint.h" +#include "snd_mix_buf.h" + +// sound engine rate defines +#define SOUND_DMA_SPEED 44100 // hardware playback rate + +#define SOUND_11k 11025 // 11khz sample rate +#define SOUND_22k 22050 // 22khz sample rate +#define SOUND_44k 44100 // 44khz sample rate +#define SOUND_ALL_RATES 1 // mix all sample rates + +#define SOUND_MIX_WET 0 // mix only samples that don't have channel set to 'dry' or 'speaker' (default) +#define SOUND_MIX_DRY 1 // mix only samples with channel set to 'dry' (ie: music) +#define SOUND_MIX_SPEAKER 2 // mix only samples with channel set to 'speaker' +#define SOUND_MIX_SPECIAL_DSP 3 // mix only samples with channel set to 'special dsp' + +#define SOUND_BUSS_ROOM (1<<0) // mix samples using channel dspmix value (based on distance from player) +#define SOUND_BUSS_FACING (1<<1) // mix samples using channel dspface value (source facing) +#define SOUND_BUSS_FACINGAWAY (1<<2) // mix samples using 1-dspface +#define SOUND_BUSS_SPEAKER (1<<3) // mix ch->bspeaker samples in mono to speaker buffer +#define SOUND_BUSS_DRY (1<<4) // mix ch->bdry samples into dry buffer +#define SOUND_BUSS_SPECIAL_DSP (1<<5) // mix ch->bspecialdsp samples into special dsp buffer + +class Vector; +struct channel_t; + +// UNDONE: Create a simulated audio device to replace the old -simsound functionality? + +// General interface to an audio device +abstract_class IAudioDevice +{ +public: + // Add a virtual destructor to silence the clang warning. + // This is harmless but not important since the only derived class + // doesn't have a destructor. + virtual ~IAudioDevice() {} + + // Detect the sound hardware and create a compatible device + // NOTE: This should NEVER fail. There is a function called Audio_GetNullDevice + // which will create a "null" device that makes no sound. If we can't create a real + // sound device, this will return a device of that type. All of the interface + // functions can be called on the null device, but it will not, of course, make sound. + static IAudioDevice *AutoDetectInit( bool waveOnly ); + + // This is needed by some of the routines to avoid doing work when you've got a null device + virtual bool IsActive( void ) = 0; + // This initializes the sound hardware. true on success, false on failure + virtual bool Init( void ) = 0; + // This releases all sound hardware + virtual void Shutdown( void ) = 0; + // stop outputting sound, but be ready to resume on UnPause + virtual void Pause( void ) = 0; + // return to normal operation after a Pause() + virtual void UnPause( void ) = 0; + // The volume of the "dry" mix (no effects). + // This should return 0 on all implementations that don't need a separate dry mix + virtual float MixDryVolume( void ) = 0; + // Should we mix sounds to a 3D (quadraphonic) sound buffer (front/rear both stereo) + virtual bool Should3DMix( void ) = 0; + + // This is called when the application stops all sounds + // NOTE: Stopping each channel and clearing the sound buffer are done separately + virtual void StopAllSounds( void ) = 0; + + // Called before painting channels, must calculated the endtime and return it (once per frame) + virtual int PaintBegin( float, int soundtime, int paintedtime ) = 0; + // Called when all channels are painted (once per frame) + virtual void PaintEnd( void ) = 0; + + // Called to set the volumes on a channel with the given gain & dot parameters + virtual void SpatializeChannel( int volume[6], int master_vol, const Vector& sourceDir, float gain, float mono ) = 0; + + // The device should apply DSP up to endtime in the current paint buffer + // this is called during painting + virtual void ApplyDSPEffects( int idsp, portable_samplepair_t *pbuffront, portable_samplepair_t *pbufrear, portable_samplepair_t *pbufcenter, int samplecount ) = 0; + + // replaces SNDDMA_GetDMAPos, gets the output sample position for tracking + virtual int GetOutputPosition( void ) = 0; + + // Fill the output buffer with silence (e.g. during pause) + virtual void ClearBuffer( void ) = 0; + + // Called each frame with the listener's coordinate system + virtual void UpdateListener( const Vector& position, const Vector& forward, const Vector& right, const Vector& up ) = 0; + + // Called each time a new paint buffer is mixed (may be multiple times per frame) + virtual void MixBegin( int sampleCount ) = 0; + virtual void MixUpsample( int sampleCount, int filtertype ) = 0; + + // sink sound data + virtual void Mix8Mono( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) = 0; + virtual void Mix8Stereo( channel_t *pChannel, char *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) = 0; + virtual void Mix16Mono( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) = 0; + virtual void Mix16Stereo( channel_t *pChannel, short *pData, int outputOffset, int inputOffset, fixedint rateScaleFix, int outCount, int timecompress ) = 0; + + // Reset a channel + virtual void ChannelReset( int entnum, int channelIndex, float distanceMod ) = 0; + virtual void TransferSamples( int end ) = 0; + + // device parameters + virtual const char *DeviceName( void ) = 0; + virtual int DeviceChannels( void ) = 0; // 1 = mono, 2 = stereo + virtual int DeviceSampleBits( void ) = 0; // bits per sample (8 or 16) + virtual int DeviceSampleBytes( void ) = 0; // above / 8 + virtual int DeviceDmaSpeed( void ) = 0; // Actual DMA speed + virtual int DeviceSampleCount( void ) = 0; // Total samples in buffer + + virtual bool IsSurround( void ) = 0; // surround enabled, could be quad or 5.1 + virtual bool IsSurroundCenter( void ) = 0; // surround enabled as 5.1 + virtual bool IsHeadphone( void ) = 0; +}; + +extern IAudioDevice *Audio_GetNullDevice( void ); + +#endif // SND_DEVICE_H diff --git a/engine/audio/public/snd_io.h b/engine/audio/public/snd_io.h new file mode 100644 index 0000000..dd74798 --- /dev/null +++ b/engine/audio/public/snd_io.h @@ -0,0 +1,16 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SND_IO_H +#define SND_IO_H +#pragma once + +class IFileReadBinary; + +extern IFileReadBinary *g_pSndIO; + +#endif // SND_IO_H diff --git a/engine/audio/public/sound.h b/engine/audio/public/sound.h new file mode 100644 index 0000000..64b4f9d --- /dev/null +++ b/engine/audio/public/sound.h @@ -0,0 +1,150 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: client sound i/o functions +// +//===========================================================================// +#ifndef SOUND_H +#define SOUND_H +#ifdef _WIN32 +#pragma once +#endif + +#include "basetypes.h" +#include "datamap.h" +#include "mathlib/vector.h" +#include "mathlib/mathlib.h" +#include "tier1/strtools.h" +#include "soundflags.h" +#include "utlvector.h" +#include "engine/SndInfo.h" + +#define MAX_SFX 2048 + +#define AUDIOSOURCE_CACHE_ROOTDIR "maps/soundcache" + +class CSfxTable; +enum soundlevel_t; +struct SoundInfo_t; +struct AudioState_t; +class IFileList; + +void S_Init (void); +void S_Shutdown (void); +bool S_IsInitted(); + +void S_StopAllSounds(bool clear); +void S_Update( const AudioState_t *pAudioState ); +void S_ExtraUpdate (void); +void S_ClearBuffer (void); +void S_BlockSound (void); +void S_UnblockSound (void); +float S_GetMasterVolume( void ); +void S_SoundFade( float percent, float holdtime, float intime, float outtime ); +void S_OnLoadScreen(bool value); +void S_EnableThreadedMixing( bool bEnable ); +void S_EnableMusic( bool bEnable ); + +struct StartSoundParams_t +{ + StartSoundParams_t() : + staticsound( false ), + userdata( 0 ), + soundsource( 0 ), + entchannel( CHAN_AUTO ), + pSfx( 0 ), + bUpdatePositions( true ), + fvol( 1.0f ), + soundlevel( SNDLVL_NORM ), + flags( SND_NOFLAGS ), + pitch( PITCH_NORM ), + specialdsp( 0 ), + fromserver( false ), + delay( 0.0f ), + speakerentity( -1 ), + suppressrecording( false ), + initialStreamPosition( 0 ) + { + origin.Init(); + direction.Init(); + } + + bool staticsound; + int userdata; + int soundsource; + int entchannel; + CSfxTable *pSfx; + Vector origin; + Vector direction; + bool bUpdatePositions; + float fvol; + soundlevel_t soundlevel; + int flags; + int pitch; + int specialdsp; + bool fromserver; + float delay; + int speakerentity; + bool suppressrecording; + int initialStreamPosition; +}; + +int S_StartSound( StartSoundParams_t& params ); +void S_StopSound ( int entnum, int entchannel ); +enum clocksync_index_t +{ + CLOCK_SYNC_CLIENT = 0, + CLOCK_SYNC_SERVER, + NUM_CLOCK_SYNCS +}; + +extern float S_ComputeDelayForSoundtime( float soundtime, clocksync_index_t syncIndex ); + +void S_StopSoundByGuid( int guid ); +float S_SoundDurationByGuid( int guid ); +int S_GetGuidForLastSoundEmitted(); +bool S_IsSoundStillPlaying( int guid ); +void S_GetActiveSounds( CUtlVector< SndInfo_t >& sndlist ); +void S_SetVolumeByGuid( int guid, float fvol ); +float S_GetElapsedTimeByGuid( int guid ); +bool S_IsLoopingSoundByGuid( int guid ); +void S_ReloadSound( const char *pSample ); +float S_GetMono16Samples( const char *pszName, CUtlVector< short >& sampleList ); + +CSfxTable *S_DummySfx( const char *name ); +CSfxTable *S_PrecacheSound (const char *sample ); +void S_PrefetchSound( char const *name, bool bPlayOnce ); +void S_MarkUISound( CSfxTable *pSfx ); +void S_ReloadFilesInList( IFileList *pFilesToReload ); + +vec_t S_GetNominalClipDist(); + +extern bool TestSoundChar(const char *pch, char c); +extern char *PSkipSoundChars(const char *pch); + +#include "soundchars.h" + +// for recording movies +void SND_MovieStart( void ); +void SND_MovieEnd( void ); + +//------------------------------------- + +int S_GetCurrentStaticSounds( SoundInfo_t *pResult, int nSizeResult, int entchannel ); + +//----------------------------------------------------------------------------- + +float S_GetGainFromSoundLevel( soundlevel_t soundlevel, vec_t dist ); + +struct musicsave_t +{ + DECLARE_SIMPLE_DATADESC(); + + char songname[ 128 ]; + int sampleposition; + short master_volume; +}; + +void S_GetCurrentlyPlayingMusic( CUtlVector< musicsave_t >& list ); +void S_RestartSong( const musicsave_t *song ); + +#endif // SOUND_H diff --git a/engine/audio/public/soundservice.h b/engine/audio/public/soundservice.h new file mode 100644 index 0000000..b1fb4df --- /dev/null +++ b/engine/audio/public/soundservice.h @@ -0,0 +1,135 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: Applicaton-level hooks for clients of the audio subsystem +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef SOUNDSERVICE_H +#define SOUNDSERVICE_H + +#if defined( _WIN32 ) +#pragma once +#endif + +class Vector; +class QAngle; +class CAudioSource; +typedef int SoundSource; +struct SpatializationInfo_t; +typedef void *FileNameHandle_t; +struct StartSoundParams_t; + +#include "utlrbtree.h" + +//----------------------------------------------------------------------------- +// Purpose: Services required by the audio system to function, this facade +// defines the bridge between the audio code and higher level +// systems. +// +// Note that some of these currently suggest that certain +// functionality would like to exist at a deeper layer so +// systems like audio can take advantage of them +// diectly (toml 05-02-02) +//----------------------------------------------------------------------------- + +abstract_class ISoundServices +{ +public: + //--------------------------------- + // Allocate a block of memory that will be automatically + // cleaned up on level change + //--------------------------------- + virtual void *LevelAlloc( int nBytes, const char *pszTag ) = 0; + + //--------------------------------- + // Notification that someone called S_ExtraUpdate() + //--------------------------------- + virtual void OnExtraUpdate() = 0; + + //--------------------------------- + // Return false if the entity doesn't exist or is out of the PVS, in which case the sound shouldn't be heard. + //--------------------------------- + virtual bool GetSoundSpatialization( int entIndex, SpatializationInfo_t& info ) = 0; + + //--------------------------------- + // This is the client's clock, which follows the servers and thus isn't 100% smooth all the time (it is in single player) + //--------------------------------- + virtual float GetClientTime() = 0; + + //--------------------------------- + // This is the engine's filtered timer, it's pretty smooth all the time + //--------------------------------- + virtual float GetHostTime() = 0; + + //--------------------------------- + //--------------------------------- + virtual int GetViewEntity() = 0; + + //--------------------------------- + //--------------------------------- + virtual float GetHostFrametime() = 0; + virtual void SetSoundFrametime( float realDt, float hostDt ) = 0; + + //--------------------------------- + //--------------------------------- + virtual int GetServerCount() = 0; + + //--------------------------------- + //--------------------------------- + virtual bool IsPlayer( SoundSource source ) = 0; + + //--------------------------------- + //--------------------------------- + virtual void OnChangeVoiceStatus( int entity, bool status) = 0; + + // Is the player fully connected (don't do DSP processing if not) + virtual bool IsConnected() = 0; + + // Calls into client .dll with list of close caption tokens to construct a caption out of + virtual void EmitSentenceCloseCaption( char const *tokenstream ) = 0; + // Calls into client .dll with list of close caption tokens to construct a caption out of + virtual void EmitCloseCaption( char const *captionname, float duration ) = 0; + + virtual char const *GetGameDir() = 0; + + // If the game is paused, certain audio will pause, too (anything with phoneme/sentence data for now) + virtual bool IsGamePaused() = 0; + + // If the game is not active, certain audio will pause + virtual bool IsGameActive() = 0; + + // restarts the sound system externally + virtual void RestartSoundSystem() = 0; + + virtual void GetAllSoundFilesReferencedInReslists( CUtlRBTree< FileNameHandle_t, int >& list ) = 0; + virtual void GetAllManifestFiles( CUtlRBTree< FileNameHandle_t, int >& list ) = 0; + virtual void GetAllSoundFilesInManifest( CUtlRBTree< FileNameHandle_t, int >& list, char const *manifestfile ) = 0; + + virtual void CacheBuildingStart() = 0; + virtual void CacheBuildingUpdateProgress( float percent, char const *cachefile ) = 0; + virtual void CacheBuildingFinish() = 0; + + // For building sound cache manifests + virtual int GetPrecachedSoundCount() = 0; + virtual char const *GetPrecachedSound( int index ) = 0; + + virtual void OnSoundStarted( int guid, StartSoundParams_t& params, char const *soundname ) = 0; + virtual void OnSoundStopped( int guid, int soundsource, int channel, char const *soundname ) = 0; + + virtual bool GetToolSpatialization( int iUserData, int guid, SpatializationInfo_t& info ) = 0; + +#if defined( _XBOX ) + virtual bool ShouldSuppressNonUISounds() = 0; +#endif + + virtual char const *GetUILanguage() = 0; +}; + +//------------------------------------- + +extern ISoundServices *g_pSoundServices; + +//============================================================================= + +#endif // SOUNDSERVICE_H diff --git a/engine/audio/public/voice.h b/engine/audio/public/voice.h new file mode 100644 index 0000000..2bfea81 --- /dev/null +++ b/engine/audio/public/voice.h @@ -0,0 +1,138 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOICE_H +#define VOICE_H +#pragma once + + +#include "ivoicetweak.h" + + +/*! @defgroup Voice Voice +Defines the engine's interface to the voice code. +@{ +*/ + +// Voice_Init will pick a sample rate, it must be within RATE_MAX +#define VOICE_OUTPUT_SAMPLE_RATE_LOW 11025 // Sample rate that we feed to the mixer. +#define VOICE_OUTPUT_SAMPLE_RATE_HIGH 22050 // Sample rate that we feed to the mixer. +#define VOICE_OUTPUT_SAMPLE_RATE_MAX 22050 // Sample rate that we feed to the mixer. + + +//! Returned on error from certain voice functions. +#define VOICE_CHANNEL_ERROR -1 +#define VOICE_CHANNEL_IN_TWEAK_MODE -2 // Returned by AssignChannel if currently in tweak mode (not an error). + + +//! Initialize the voice code. +bool Voice_Init( const char *pCodec, int nSampleRate ); + +//! Inits voice with defaults if it is not initialized normally, e.g. for local mixer use. +void Voice_ForceInit(); + +//! Get the default sample rate to use for this codec +inline int Voice_GetDefaultSampleRate( const char *pCodec ) // Inline for DEDICATED builds +{ + // Use legacy lower rate for speex + if ( Q_stricmp( pCodec, "vaudio_speex" ) == 0 ) + { + return VOICE_OUTPUT_SAMPLE_RATE_LOW; + } + else if ( Q_stricmp( pCodec, "steam" ) == 0 ) + { + return 0; // For the steam codec, 0 passed to voice_init means "use optimal steam voice rate" + } + + // Use high sample rate by default for other codecs. + return VOICE_OUTPUT_SAMPLE_RATE_HIGH; +} + +//! Shutdown the voice code. +void Voice_Deinit(); + +//! Returns true if the client has voice enabled +bool Voice_Enabled( void ); + +//! The codec voice was initialized with. Empty string if voice is not initialized. +const char *Voice_ConfiguredCodec(); + +//! The sample rate voice was initialized with. -1 if voice is not initialized. +int Voice_ConfiguredSampleRate(); + +//! Returns true if the user can hear themself speak. +bool Voice_GetLoopback(); + + +//! This is called periodically by the engine when the server acks the local player talking. +//! This tells the client DLL that the local player is talking and fades after about 200ms. +void Voice_LocalPlayerTalkingAck(); + + +//! Call every frame to update the voice stuff. +void Voice_Idle(float frametime); + + +//! Returns true if mic input is currently being recorded. +bool Voice_IsRecording(); + +//! Begin recording input from the mic. +bool Voice_RecordStart( + //! Filename to store incoming mic data, or NULL if none. + const char *pUncompressedFile, + + //! Filename to store the output of compression and decompressiong with the codec, or NULL if none. + const char *pDecompressedFile, + + //! If this is non-null, the voice manager will use this file for input instead of the mic. + const char *pMicInputFile + ); + +// User wants to stop recording +void Voice_UserDesiresStop(); + +//! Stop recording from the mic. +bool Voice_RecordStop(); + + +//! Get the most recent N bytes of compressed data. If nCount is less than the number of +//! available bytes, it discards the first bytes and gives you the last ones. +//! Set bFinal to true on the last call to this (it will flush out any stored voice data). +int Voice_GetCompressedData(char *pchData, int nCount, bool bFinal); + + + +//! Pass incoming data from the server into here. +//! The data should have been compressed and gotten through a Voice_GetCompressedData call. +int Voice_AddIncomingData( + //! Channel index. + int nChannel, + //! Compressed data to add to the channel. + const char *pchData, + //! Number of bytes in pchData. + int nCount, + //! Sequence number. If a packet is missed, it adds padding so the time isn't squashed. + int iSequenceNumber + ); + +//! Call this to reserve a voice channel for the specified entity to talk into. +//! \return A channel index for use with Voice_AddIncomingData or VOICE_CHANNEL_ERROR on error. +int Voice_AssignChannel(int nEntity, bool bProximity ); + +//! Call this to get the channel index that the specified entity is talking into. +//! \return A channel index for use with Voice_AddIncomingData or VOICE_CHANNEL_ERROR if the entity isn't talking. +int Voice_GetChannel(int nEntity); + +#if !defined( NO_VOICE ) +extern IVoiceTweak g_VoiceTweakAPI; +extern bool g_bUsingSteamVoice; +#endif + +/*! @} */ + + +#endif // VOICE_H diff --git a/engine/audio/public/vox.h b/engine/audio/public/vox.h new file mode 100644 index 0000000..6b50008 --- /dev/null +++ b/engine/audio/public/vox.h @@ -0,0 +1,46 @@ +//========= Copyright Valve Corporation, All rights reserved. ============// +// +// Purpose: +// +// $Workfile: $ +// $Date: $ +// +//----------------------------------------------------------------------------- +// $Log: $ +// +// $NoKeywords: $ +//=============================================================================// + +#ifndef VOX_H +#define VOX_H +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +struct sfxcache_t; +struct channel_t; + +class CUtlSymbol; + +extern void VOX_Init( void ); +extern void VOX_Shutdown( void ); +extern void VOX_ReadSentenceFile( const char *psentenceFileName ); +extern int VOX_SentenceCount( void ); +extern void VOX_LoadSound( channel_t *pchan, const char *psz ); +// UNDONE: Improve the interface of this call, it returns sentence data AND the sentence index +extern char *VOX_LookupString( const char *pSentenceName, int *psentencenum, bool *pbEmitCaption = NULL, CUtlSymbol *pCaptionSymbol = NULL, float * pflDuration = NULL ); +extern void VOX_PrecacheSentenceGroup( class IEngineSound *pSoundSystem, const char *pGroupName, const char *pPathOverride = NULL ); +extern const char *VOX_SentenceNameFromIndex( int sentencenum ); +extern float VOX_SentenceLength( int sentence_num ); +extern const char *VOX_GroupNameFromIndex( int groupIndex ); +extern int VOX_GroupIndexFromName( const char *pGroupName ); +extern int VOX_GroupPick( int isentenceg, char *szfound, int strLen ); +extern int VOX_GroupPickSequential( int isentenceg, char *szfound, int szfoundLen, int ipick, int freset ); + +#ifdef __cplusplus +} +#endif + +#endif // VOX_H |