From ee254b984fac1e89e8e4014ac0a8e8c090f7399f Mon Sep 17 00:00:00 2001 From: Jordan Cristiano Date: Fri, 30 Mar 2018 17:17:16 -0400 Subject: [PATCH] Added voice data writer. Rips voice chat from svc_VoiceData and writes it to separate wav files. --- demboyz/io/idemowriter.h | 1 + demboyz/io/voicewriter/voicedatawriter.cpp | 290 +++++++++++++++++++++ demboyz/io/voicewriter/wavfilewriter.h | 89 +++++++ premake/premake5.lua | 1 + 4 files changed, 381 insertions(+) create mode 100644 demboyz/io/voicewriter/voicedatawriter.cpp create mode 100644 demboyz/io/voicewriter/wavfilewriter.h diff --git a/demboyz/io/idemowriter.h b/demboyz/io/idemowriter.h index ed68254..d38bb82 100644 --- a/demboyz/io/idemowriter.h +++ b/demboyz/io/idemowriter.h @@ -46,6 +46,7 @@ public: static IDemoWriter* CreateJsonWriter(void* outputFp); static IDemoWriter* CreateDemoWriter(void* outputFp); static IDemoWriter* CreateConLogWriter(void* outputFp); + static IDemoWriter* CreateVoiceDataWriter(const char* outputPath); static void FreeDemoWriter(IDemoWriter* writer) { diff --git a/demboyz/io/voicewriter/voicedatawriter.cpp b/demboyz/io/voicewriter/voicedatawriter.cpp new file mode 100644 index 0000000..620746e --- /dev/null +++ b/demboyz/io/voicewriter/voicedatawriter.cpp @@ -0,0 +1,290 @@ + +#include "../idemowriter.h" +#include "netmessages/netmessages.h" + +#include "netmessages/svc_voiceinit.h" +#include "netmessages/svc_voicedata.h" + +#include "celt/celt.h" + +#include + +#include "wavfilewriter.h" + +#define USE_VAUDIO_CELT +#define MAX_PLAYERS 33 + +#ifdef USE_VAUDIO_CELT +#define VC_EXTRALEAN +#define WIN32_LEAN_AND_MEAN +#include +#endif + +struct CeltConfig +{ + celt_int32 sampleRate; + uint32_t frameSizeSamples; + uint32_t encodedFrameSizeBytes; +}; + +static CeltConfig sCeltConfigs[] = +{ + { 44100, 256, 120 }, // unused + { 22050, 120, 60 }, // unused + { 22050, 256, 60 }, // unused + { 22050, 512, 64 }, // vaudio_celt + { 44100, 1024, 128 } // vaudio_celt_high +}; + +#ifdef USE_VAUDIO_CELT + +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; +}; + +typedef void* (CreateInterfaceFn)(const char *pName, int *pReturnCode); +static HINSTANCE celtDll; +static CreateInterfaceFn* createInterfaceFunc; + +#else + +class CeltVoiceDecoder +{ +public: + bool DoInit(CELTMode* celtMode, uint32_t frameSizeSamples, uint32_t encodedFrameSizeBytes) + { + if(m_celtDecoder) + { + return false; + } + + int error = CELT_OK; + m_celtDecoder = celt_decoder_create_custom(celtMode, sCeltChannels, &error); + assert(error == CELT_OK); + assert(m_celtDecoder); + + m_frameSizeSamples = frameSizeSamples; + m_encodedFrameSizeBytes = encodedFrameSizeBytes; + return true; + } + + void Destroy() + { + celt_decoder_destroy(m_celtDecoder); + m_celtDecoder = NULL; + } + + void Reset() + { + } + + int Decompress( + const uint8_t* compressedData, + int compressedBytes, + int16_t* uncompressedData, + int maxUncompressedSamples) + { + int curCompressedByte = 0; + int curDecompressedSample = 0; + + const uint32_t encodedframeSizeBytes = m_encodedFrameSizeBytes; + const uint32_t frameSizeSamples = m_frameSizeSamples; + while( + ((compressedBytes - curCompressedByte) >= encodedframeSizeBytes) && + ((maxUncompressedSamples - curDecompressedSample) >= frameSizeSamples)) + { + DecodeFrame(&compressedData[curCompressedByte], &uncompressedData[curDecompressedSample]); + curCompressedByte += encodedframeSizeBytes; + curDecompressedSample += frameSizeSamples; + } + return curDecompressedSample; + } + +private: + void DecodeFrame(const uint8_t* compressedData, int16_t* uncompressedData) + { + int error = celt_decode(m_celtDecoder, compressedData, m_encodedFrameSizeBytes, uncompressedData, m_frameSizeSamples); + assert(error >= CELT_OK); + } + +private: + static const int sCeltChannels = 1; + +private: + CELTDecoder* m_celtDecoder = NULL; + uint32_t m_frameSizeSamples; + uint32_t m_encodedFrameSizeBytes; +}; + +#endif // USE_VAUDIO_CELT + +class VoiceDataWriter: public IDemoWriter +{ +public: + VoiceDataWriter(const char* outputPath); + + virtual void StartWriting(demoheader_t& header) override final; + virtual void EndWriting() override final; + + virtual void StartCommandPacket(const CommandPacket& packet) override final; + virtual void EndCommandPacket(const PacketTrailingBits& trailingBits) override final; + + virtual void WriteNetPacket(NetPacket& packet, SourceGameContext& context) override final; + +private: + struct PlayerVoiceState + { +#ifdef USE_VAUDIO_CELT + IVoiceCodec* celtDecoder; +#else + CeltVoiceDecoder decoder; +#endif + WaveFileWriter wavWriter; + int32_t lastVoiceDataTick = -1; + }; + +private: + CELTMode* m_celtMode; + PlayerVoiceState m_playerVoiceStates[MAX_PLAYERS]; + + int32_t m_curTick; + const char* m_outputPath; + + int16_t m_decodeBuffer[8192]; + + static const int sQuality = 3; +}; + +IDemoWriter* IDemoWriter::CreateVoiceDataWriter(const char* outputPath) +{ + return new VoiceDataWriter(outputPath); +} + +VoiceDataWriter::VoiceDataWriter(const char* outputPath): + m_celtMode(nullptr), + m_playerVoiceStates(), + m_curTick(-1), + m_outputPath(outputPath) +{ +} + +void VoiceDataWriter::StartWriting(demoheader_t& header) +{ +#ifdef USE_VAUDIO_CELT + celtDll = LoadLibrary(TEXT("vaudio_celt.dll")); + createInterfaceFunc = (CreateInterfaceFn*)GetProcAddress(celtDll, "CreateInterface"); +#else + int error = CELT_OK; + const CeltConfig& config = sCeltConfigs[sQuality]; + m_celtMode = celt_mode_create(config.sampleRate, config.frameSizeSamples, &error); + assert(error == CELT_OK); + assert(m_celtMode); +#endif +} + +void VoiceDataWriter::EndWriting() +{ + for(PlayerVoiceState& state : m_playerVoiceStates) + { +#ifdef USE_VAUDIO_CELT + if(state.celtDecoder) + { + state.celtDecoder->Release(); + } +#else + state.decoder.Destroy(); +#endif + state.wavWriter.Close(); + state.lastVoiceDataTick = -1; + } +#ifndef USE_VAUDIO_CELT + if(m_celtMode) + { + celt_mode_destroy(m_celtMode); + m_celtMode = nullptr; + } +#endif +} + +void VoiceDataWriter::StartCommandPacket(const CommandPacket& packet) +{ + m_curTick = packet.tick; +} + +void VoiceDataWriter::EndCommandPacket(const PacketTrailingBits& trailingBits) +{ +} + +void VoiceDataWriter::WriteNetPacket(NetPacket& packet, SourceGameContext& context) +{ + if(packet.type == NetMsg::svc_VoiceInit) + { + NetMsg::SVC_VoiceInit* voiceInit = static_cast(packet.data); + assert(!strcmp(voiceInit->voiceCodec, "vaudio_celt")); + assert(voiceInit->quality == NetMsg::SVC_VoiceInit::QUALITY_HAS_SAMPLE_RATE); + assert(voiceInit->sampleRate == sCeltConfigs[sQuality].sampleRate); + } + else if(packet.type == NetMsg::svc_VoiceData) + { + NetMsg::SVC_VoiceData* voiceData = static_cast(packet.data); + assert(voiceData->fromClientIndex < MAX_PLAYERS); + + PlayerVoiceState& state = m_playerVoiceStates[voiceData->fromClientIndex]; + + const CeltConfig& config = sCeltConfigs[sQuality]; +#ifdef USE_VAUDIO_CELT + const bool initWavWriter = !state.celtDecoder; + if(!state.celtDecoder) + { + int ret = 0; + state.celtDecoder = static_cast(createInterfaceFunc("vaudio_celt", &ret)); + state.celtDecoder->Init(sQuality); + } +#else + const bool initWavWriter = state.decoder.DoInit(m_celtMode, config.frameSizeSamples, config.encodedFrameSizeBytes); +#endif + if(initWavWriter) + { + std::string name = std::string(m_outputPath) + "/client_" + std::to_string((uint32_t)voiceData->fromClientIndex) + ".wav"; + state.wavWriter.Init(name.c_str(), config.sampleRate); + assert(state.lastVoiceDataTick == -1); + state.lastVoiceDataTick = m_curTick; + } + + assert((voiceData->dataLengthInBits % 8) == 0); + const int numBytes = voiceData->dataLengthInBits / 8; + +#ifdef USE_VAUDIO_CELT + const int numDecompressedSamples = state.celtDecoder->Decompress((const char*)voiceData->data.get(), numBytes, (char*)m_decodeBuffer, 8192*2); +#else + const int numDecompressedSamples = state.decoder.Decompress(voiceData->data.get(), numBytes, m_decodeBuffer, 8192); +#endif + state.wavWriter.WriteSamples(m_decodeBuffer, numDecompressedSamples); + + state.lastVoiceDataTick = m_curTick; + } +} diff --git a/demboyz/io/voicewriter/wavfilewriter.h b/demboyz/io/voicewriter/wavfilewriter.h new file mode 100644 index 0000000..c367581 --- /dev/null +++ b/demboyz/io/voicewriter/wavfilewriter.h @@ -0,0 +1,89 @@ + +#pragma once + +#include +#include +#include + +class WaveFileWriter +{ +public: + WaveFileWriter(): + m_file(nullptr) + { + } + + ~WaveFileWriter() + { + assert(!m_file); + } + + void Init(const char* file, uint32_t sampleRate) + { + FILE* fp = fopen(file, "wb"); + assert(fp); + + m_file = fp; + m_DataBytes = 0; + + const uint32_t chunkSize = 0; + + const uint32_t fmtChunkSize = 16; + const uint16_t audioFormat = 1; // pcm + const uint16_t numChannels = 1; + const uint32_t bytesPerSample = 2; + const uint32_t byteRate = sampleRate * numChannels * bytesPerSample; + const uint16_t blockAlign = numChannels * bytesPerSample; + const uint16_t bitsPerSample = 16; + + fputs("RIFF", fp); + fwrite(&chunkSize, sizeof(chunkSize), 1, fp); + fputs("WAVE", fp); + + fputs("fmt ", fp); + fwrite(&fmtChunkSize, sizeof(fmtChunkSize), 1, fp); + fwrite(&audioFormat, sizeof(audioFormat), 1, fp); + fwrite(&numChannels, sizeof(numChannels), 1, fp); + fwrite(&sampleRate, sizeof(sampleRate), 1, fp); + fwrite(&byteRate, sizeof(byteRate), 1, fp); + fwrite(&blockAlign, sizeof(blockAlign), 1, fp); + fwrite(&bitsPerSample, sizeof(bitsPerSample), 1, fp); + + fputs("data", fp); + fwrite(&chunkSize, sizeof(chunkSize), 1, fp); + } + + void Close() + { + FILE* fp = m_file; + if(!fp) + { + return; + } + + const uint32_t dataSize = m_DataBytes; + const uint32_t chunkSize = dataSize + 36; + + fseek(fp, 4, SEEK_SET); + fwrite(&chunkSize, sizeof(chunkSize), 1, fp); + + fseek(fp, 40, SEEK_SET); + fwrite(&dataSize, sizeof(dataSize), 1, fp); + + fclose(fp); + + m_file = nullptr; + } + + void WriteSamples(const int16_t* samples, uint32_t numSamples) + { + const uint32_t bytesPerSample = 2; + const size_t elemsWritten = fwrite(samples, bytesPerSample, numSamples, m_file); + assert(elemsWritten == numSamples); + m_DataBytes += (elemsWritten * bytesPerSample); + } + +private: + FILE* m_file; + uint32_t m_DataBytes; +}; diff --git a/premake/premake5.lua b/premake/premake5.lua index 5e9cdb4..f90a400 100644 --- a/premake/premake5.lua +++ b/premake/premake5.lua @@ -34,6 +34,7 @@ solution "demboyz" "../external/sourcesdk/include", "../external/rapidjson-1.0.2/include", "../external/snappy-1.1.3/include", + "../external/celt-e18de77/include", "../demboyz" } links