Added voice data writer. Rips voice chat from svc_VoiceData and writes it to separate wav files.
This commit is contained in:
parent
24fb8be512
commit
ee254b984f
@ -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)
|
||||||
{
|
{
|
||||||
|
290
demboyz/io/voicewriter/voicedatawriter.cpp
Normal file
290
demboyz/io/voicewriter/voicedatawriter.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
89
demboyz/io/voicewriter/wavfilewriter.h
Normal file
89
demboyz/io/voicewriter/wavfilewriter.h
Normal 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;
|
||||||
|
};
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user