Skip to content

Commit

Permalink
Add voice extraction example (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
in0finite authored Oct 12, 2024
1 parent 2b2ac7b commit 457bec3
Show file tree
Hide file tree
Showing 3 changed files with 148 additions and 0 deletions.
8 changes: 8 additions & 0 deletions demofile-net.sln
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@

Microsoft Visual Studio Solution File, Format Version 12.00
#
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{42F3F9ED-D28F-4560-95E9-054FA9424DB6}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
Expand Down Expand Up @@ -50,6 +51,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoFile.Example.Basic.Coun
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoFile.Example.MultiThreaded", "examples\DemoFile.Example.MultiThreaded\DemoFile.Example.MultiThreaded.csproj", "{014CF79F-F046-407D-8A20-DC5634633188}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DemoFile.Example.VoiceExtraction", "examples\DemoFile.Example.VoiceExtraction\DemoFile.Example.VoiceExtraction.csproj", "{462546EB-6090-4585-A641-2FCEC22A3BD9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -124,6 +127,10 @@ Global
{014CF79F-F046-407D-8A20-DC5634633188}.Debug|Any CPU.Build.0 = Debug|Any CPU
{014CF79F-F046-407D-8A20-DC5634633188}.Release|Any CPU.ActiveCfg = Release|Any CPU
{014CF79F-F046-407D-8A20-DC5634633188}.Release|Any CPU.Build.0 = Release|Any CPU
{462546EB-6090-4585-A641-2FCEC22A3BD9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{462546EB-6090-4585-A641-2FCEC22A3BD9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{462546EB-6090-4585-A641-2FCEC22A3BD9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{462546EB-6090-4585-A641-2FCEC22A3BD9}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{C2452138-91E4-4160-B051-A201080C1953} = {20EAA685-6916-4B0C-8D0C-E407B072E28F}
Expand All @@ -135,5 +142,6 @@ Global
{AE2FD960-2970-42F8-904D-F3C6B9E10623} = {20EAA685-6916-4B0C-8D0C-E407B072E28F}
{6CDE3A80-C8B5-4007-83F4-0FDFB9FF4B87} = {20EAA685-6916-4B0C-8D0C-E407B072E28F}
{014CF79F-F046-407D-8A20-DC5634633188} = {20EAA685-6916-4B0C-8D0C-E407B072E28F}
{462546EB-6090-4585-A641-2FCEC22A3BD9} = {20EAA685-6916-4B0C-8D0C-E407B072E28F}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Concentus" Version="2.2.2" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\DemoFile.Game.Cs\DemoFile.Game.Cs.csproj" />
</ItemGroup>

</Project>
121 changes: 121 additions & 0 deletions examples/DemoFile.Example.VoiceExtraction/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
using Concentus;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;

namespace DemoFile.Example.VoiceExtraction
{
internal class Program
{
public static async Task Main(string[] args)
{
var path = args.SingleOrDefault() ?? throw new Exception("Expected a single argument: <path to .dem>");

var demo = new CsDemoParser();
var demoFileReader = new DemoFileReader<CsDemoParser>(demo, new MemoryStream(File.ReadAllBytes(path)));

var stopwatch = Stopwatch.StartNew();

Dictionary<ulong, List<CMsgVoiceAudio>> voiceDataPerSteamId = new();

demo.PacketEvents.SvcVoiceData += e =>
{
if (e.Audio == null)
return;

// after 6th Feb 2024, CS uses Opus format, before that it was Steam format
if (e.Audio.Format != VoiceDataFormat_t.VoicedataFormatOpus)
throw new ArgumentException($"Invalid voice format: {e.Audio.Format}");

voiceDataPerSteamId.TryGetValue(e.Xuid, out var voiceData);
voiceData ??= new();
voiceData.Add(e.Audio);
voiceDataPerSteamId[e.Xuid] = voiceData;
};

await demoFileReader.ReadAllAsync(CancellationToken.None);

Console.WriteLine($"Extracting voice to directory: {Directory.GetCurrentDirectory()}\n");

Console.WriteLine($"Total players with voice {voiceDataPerSteamId.Count}, " +
$"total messages {voiceDataPerSteamId.Sum(_ => _.Value.Count)}, " +
$"total compressed size {voiceDataPerSteamId.Sum(_ => _.Value.Sum(a => a.VoiceData.Length)) / 1024} KB\n");

const int k_sampleRate = 48000;
const int k_numChannels = 1;
long totalSizeExtracted = 0;

OpusCodecFactory.AttemptToUseNativeLibrary = false; // make sure that Managed-only code works
using var decoder = OpusCodecFactory.CreateDecoder(k_sampleRate, k_numChannels);

foreach (var item in voiceDataPerSteamId)
{
ulong steamId = item.Key;
var player = demo.GetPlayerBySteamId(steamId);

List<CMsgVoiceAudio> audioMessages = item.Value;
int compressedSize = audioMessages.Sum(_ => _.VoiceData.Length);
float[] pcmSamples = new float[compressedSize * 64]; // should be enough
int numDecodedSamples = 0;
foreach (var audioMessage in audioMessages)
{
if (audioMessage.VoiceData.Length == 0) // necessary to check this, because otherwise Decoder can allocate big array for no reason
continue;
int numSamplesDecodedInMessage = decoder.Decode(audioMessage.VoiceData.Span, pcmSamples.AsSpan(numDecodedSamples), pcmSamples.Length - numDecodedSamples);
numDecodedSamples += numSamplesDecodedInMessage;
}

WriteWavFile($"{steamId}_demo_voice.wav", k_sampleRate, k_numChannels, pcmSamples.AsSpan(0, numDecodedSamples));

int sizeExtracted = numDecodedSamples * 4;
totalSizeExtracted += sizeExtracted;

Console.WriteLine($"{player?.PlayerName ?? steamId.ToString()}: {audioMessages.Count} messages, {compressedSize / 1024} KB compressed, {sizeExtracted / 1024} KB decompressed");
}

Console.WriteLine($"\nTotal voice extracted: {totalSizeExtracted / 1024.0:F} KB\n");

Console.WriteLine($"Finished! elapsed {stopwatch.Elapsed.TotalSeconds:F} sec, ticks {demo.CurrentDemoTick.Value}");
}

static void WriteWavFile(string filePath, int sampleRate, int numChannels, ReadOnlySpan<float> samplesFloat32)
{
int numSamples = samplesFloat32.Length;
int sampleSize = sizeof(int);

int[] samplesInt32 = new int[numSamples];
const int conversionScale = int.MaxValue - 1;
for (int i = 0; i < numSamples; i++)
samplesInt32[i] = (int)(samplesFloat32[i] * conversionScale);

WriteWavFile(filePath, numSamples, sampleRate, numChannels, sampleSize, MemoryMarshal.AsBytes(samplesInt32.AsSpan()));
}

static void WriteWavFile(
string filePath, int numSamples, int sampleRate, int numChannels, int sampleSize, ReadOnlySpan<byte> audioData)
{
var stream = new MemoryStream(44 + numSamples * sampleSize * numChannels);
var wr = new BinaryWriter(stream);

wr.Write(Encoding.ASCII.GetBytes("RIFF"));
wr.Write(36 + numSamples * numChannels * sampleSize);
wr.Write(Encoding.ASCII.GetBytes("WAVEfmt "));
wr.Write((int)16);
wr.Write((short)1); // Encoding
wr.Write((short)numChannels); // Channels
wr.Write((int)(sampleRate)); // Sample rate
wr.Write((int)(sampleRate * sampleSize * numChannels)); // Average bytes per second
wr.Write((short)(sampleSize * numChannels)); // block align
wr.Write((short)(8 * sampleSize)); // bits per sample
wr.Write(Encoding.ASCII.GetBytes("data")); // data chunk id
wr.Write((int)(numSamples * sampleSize * numChannels)); // data size

if (stream.Position != 44)
throw new UnreachableException("Header not filled correctly");

stream.Write(audioData);

File.WriteAllBytes(filePath, stream.GetBuffer());
}
}
}

0 comments on commit 457bec3

Please sign in to comment.