Added voice data writer. Rips voice chat from svc_VoiceData and writes it to separate wav files.

This commit is contained in:
Jordan Cristiano 2018-03-30 17:17:16 -04:00
parent 24fb8be512
commit ee254b984f
4 changed files with 381 additions and 0 deletions

View File

@ -46,6 +46,7 @@ public:
static IDemoWriter* CreateJsonWriter(void* outputFp); static IDemoWriter* CreateJsonWriter(void* outputFp);
static IDemoWriter* CreateDemoWriter(void* outputFp); static IDemoWriter* CreateDemoWriter(void* outputFp);
static IDemoWriter* CreateConLogWriter(void* outputFp); static IDemoWriter* CreateConLogWriter(void* outputFp);
static IDemoWriter* CreateVoiceDataWriter(const char* outputPath);
static void FreeDemoWriter(IDemoWriter* writer) static void FreeDemoWriter(IDemoWriter* writer)
{ {

View File

@ -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 <cassert>
#include "wavfilewriter.h"
#define USE_VAUDIO_CELT
#define MAX_PLAYERS 33
#ifdef USE_VAUDIO_CELT
#define VC_EXTRALEAN
#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#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<NetMsg::SVC_VoiceInit*>(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<NetMsg::SVC_VoiceData*>(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<IVoiceCodec*>(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;
}
}

View File

@ -0,0 +1,89 @@
#pragma once
#include <stdio.h>
#include <cassert>
#include <cstdint>
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;
};

View File

@ -34,6 +34,7 @@ solution "demboyz"
"../external/sourcesdk/include", "../external/sourcesdk/include",
"../external/rapidjson-1.0.2/include", "../external/rapidjson-1.0.2/include",
"../external/snappy-1.1.3/include", "../external/snappy-1.1.3/include",
"../external/celt-e18de77/include",
"../demboyz" "../demboyz"
} }
links links