├── .gitignore ├── README.md ├── cs2-voice-data.go ├── decoder ├── chunk.go └── decoder.go ├── go.mod └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | *.dll 2 | *.dylib 3 | *.bin 4 | *.wav 5 | *.dem 6 | bin 7 | .DS_Store 8 | 9 | # local env files 10 | .env.local 11 | .env.*.local 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea/ 21 | venv 22 | .vscode 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | *.env 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS2 Voice extractor 2 | 3 | Example code for exporting players voices from CS2 demos into WAV files. 4 | 5 | **Valve Matchmaking demos do not contain voice audio data, as such there is nothing to extract from MM demo files.** 6 | 7 | 8 | ## Purpose and goals 9 | The intention of this project is not to be an end user tool for bulk or batch processing demos and extracting voice data. 10 | 11 | However, this should serve as a guideline for how to process the audio data as pulled by 12 | [demoinfocs-golang](https://github.com/markus-wa/demoinfocs-golang). People using that tool to process their demos 13 | who wish to also pull voice data can leverage this sample to build that audio processing into their demo processing 14 | tools. 15 | 16 | 17 | ## Setup and processing 18 | 1. Pulling all the required dependencies. 19 | 2. `go get ./...` 20 | 3. Update the cs2-voide-data.go file with the path to your unzipped demo file. 21 | 4. Running the sample 22 | 5. `go run cs2-voice-data.go` 23 | 24 | 25 | ## Dependencies 26 | This project does have a dependency on lib opus, which is easy to install on mac/linux. 27 | 28 | Linux: 29 | ```sh 30 | sudo apt-get install pkg-config libopus-dev libopusfile-dev 31 | ``` 32 | 33 | Mac: 34 | ```sh 35 | brew install pkg-config opus opusfile 36 | ``` 37 | 38 | As for direct application dependencies those are all handled by the go.mod and are all pulled doing the `go get ./...` from step 2 above. 39 | 40 | # Acknowledgements 41 | 42 | Thanks to [@rumblefrog](https://github.com/rumblefrog) for all their help in getting this working. Check out this excellent blog post about [Reversing Steam Voice Codec](https://zhenyangli.me/posts/reversing-steam-voice-codec/) and their work on [Source Chat Relay](https://github.com/rumblefrog/source-chat-relay) 43 | 44 | This sample relies on [demoinfocs-golang](https://github.com/markus-wa/demoinfocs-golang). Thank you to [@markus-wa](https://github.com/markus-wa), [@akiver](https://github.com/akiver) and all the contributors there. 45 | -------------------------------------------------------------------------------- /cs2-voice-data.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "CS2VoiceData/decoder" 5 | "fmt" 6 | "log" 7 | "os" 8 | "strconv" 9 | 10 | "github.com/go-audio/audio" 11 | "github.com/go-audio/wav" 12 | dem "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs" 13 | "github.com/markus-wa/demoinfocs-golang/v4/pkg/demoinfocs/msgs2" 14 | ) 15 | 16 | func main() { 17 | // Create a map of a users to voice data. 18 | // Each chunk of voice data is a slice of bytes, store all those slices in a grouped slice. 19 | var voiceDataPerPlayer = map[string][][]byte{} 20 | 21 | // The file path to an unzipped demo file. 22 | file, err := os.Open("1-34428882-6181-4c75-a24b-4982764122e2.dem") 23 | if err != nil { 24 | log.Fatal("Failed to open demo file") 25 | } 26 | defer file.Close() 27 | 28 | parser := dem.NewParser(file) 29 | var format string 30 | 31 | // Add a parser register for the VoiceData net message. 32 | parser.RegisterNetMessageHandler(func(m *msgs2.CSVCMsg_VoiceData) { 33 | // Get the users Steam ID 64. 34 | steamId := strconv.Itoa(int(m.GetXuid())) 35 | // Append voice data to map 36 | format = m.Audio.Format.String() 37 | voiceDataPerPlayer[steamId] = append(voiceDataPerPlayer[steamId], m.Audio.VoiceData) 38 | }) 39 | 40 | // Parse the full demo file. 41 | err = parser.ParseToEnd() 42 | 43 | // For each users data, create a wav file containing their voice comms. 44 | for playerId, voiceData := range voiceDataPerPlayer { 45 | wavFilePath := fmt.Sprintf("%s.wav", playerId) 46 | if format == "VOICEDATA_FORMAT_OPUS" { 47 | err = opusToWav(voiceData, wavFilePath) 48 | if err != nil { 49 | fmt.Println(err) 50 | continue 51 | } 52 | 53 | } else if format == "VOICEDATA_FORMAT_STEAM" { 54 | convertAudioDataToWavFiles(voiceData, wavFilePath) 55 | } 56 | } 57 | 58 | defer parser.Close() 59 | } 60 | 61 | func convertAudioDataToWavFiles(payloads [][]byte, fileName string) { 62 | // This sample rate can be set using data from the VoiceData net message. 63 | // But every demo processed has used 24000 and is single channel. 64 | voiceDecoder, err := decoder.NewOpusDecoder(24000, 1) 65 | 66 | if err != nil { 67 | fmt.Println(err) 68 | } 69 | 70 | o := make([]int, 0, 1024) 71 | 72 | for _, payload := range payloads { 73 | c, err := decoder.DecodeChunk(payload) 74 | 75 | if err != nil { 76 | fmt.Println(err) 77 | } 78 | 79 | // Not silent frame 80 | if c != nil && len(c.Data) > 0 { 81 | pcm, err := voiceDecoder.Decode(c.Data) 82 | 83 | if err != nil { 84 | fmt.Println(err) 85 | } 86 | 87 | converted := make([]int, len(pcm)) 88 | for i, v := range pcm { 89 | // Float32 buffer implementation is wrong in go-audio, so we have to convert to int before encoding 90 | converted[i] = int(v * 2147483647) 91 | } 92 | 93 | o = append(o, converted...) 94 | } 95 | } 96 | 97 | outFile, err := os.Create(fileName) 98 | 99 | if err != nil { 100 | fmt.Println(err) 101 | } 102 | defer outFile.Close() 103 | 104 | // Encode new wav file, from decoded opus data. 105 | enc := wav.NewEncoder(outFile, 24000, 32, 1, 1) 106 | 107 | buf := &audio.IntBuffer{ 108 | Data: o, 109 | Format: &audio.Format{ 110 | SampleRate: 24000, 111 | NumChannels: 1, 112 | }, 113 | } 114 | 115 | // Write voice data to the file. 116 | if err := enc.Write(buf); err != nil { 117 | fmt.Println(err) 118 | } 119 | 120 | enc.Close() 121 | } 122 | 123 | func opusToWav(data [][]byte, wavName string) (err error) { 124 | opusDecoder, err := decoder.NewDecoder(48000, 1) 125 | if err != nil { 126 | return 127 | } 128 | 129 | var pcmBuffer []int 130 | 131 | for _, d := range data { 132 | pcm, err := decoder.Decode(opusDecoder, d) 133 | if err != nil { 134 | log.Println(err) 135 | continue 136 | } 137 | 138 | pp := make([]int, len(pcm)) 139 | 140 | for i, p := range pcm { 141 | pp[i] = int(p * 2147483647) 142 | } 143 | 144 | pcmBuffer = append(pcmBuffer, pp...) 145 | } 146 | 147 | file, err := os.Create(wavName) 148 | if err != nil { 149 | return 150 | } 151 | defer file.Close() 152 | 153 | enc := wav.NewEncoder(file, 48000, 32, 1, 1) 154 | defer enc.Close() 155 | 156 | buffer := &audio.IntBuffer{ 157 | Data: pcmBuffer, 158 | Format: &audio.Format{ 159 | SampleRate: 48000, 160 | NumChannels: 1, 161 | }, 162 | } 163 | 164 | err = enc.Write(buffer) 165 | if err != nil { 166 | return 167 | } 168 | 169 | return 170 | } 171 | -------------------------------------------------------------------------------- /decoder/chunk.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "hash/crc32" 9 | ) 10 | 11 | const ( 12 | minimumLength = 18 13 | ) 14 | 15 | var ( 16 | ErrInsufficientData = errors.New("insufficient amount of data to chunk") 17 | ErrInvalidVoicePacket = errors.New("invalid voice packet") 18 | ErrMismatchChecksum = errors.New("mismatching voice data checksum") 19 | ) 20 | 21 | type Chunk struct { 22 | SteamID uint64 23 | SampleRate uint16 24 | Length uint16 25 | Data []byte 26 | Checksum uint32 27 | } 28 | 29 | func DecodeChunk(b []byte) (*Chunk, error) { 30 | bLen := len(b) 31 | 32 | if bLen < minimumLength { 33 | return nil, fmt.Errorf("%w (received: %d bytes, expected at least %d bytes)", ErrInsufficientData, bLen, minimumLength) 34 | } 35 | 36 | chunk := &Chunk{} 37 | 38 | buf := bytes.NewBuffer(b) 39 | 40 | if err := binary.Read(buf, binary.LittleEndian, &chunk.SteamID); err != nil { 41 | return nil, err 42 | } 43 | 44 | var payloadType byte 45 | if err := binary.Read(buf, binary.LittleEndian, &payloadType); err != nil { 46 | return nil, err 47 | } 48 | 49 | if payloadType != 0x0B { 50 | return nil, fmt.Errorf("%w (received %x, expected %x)", ErrInvalidVoicePacket, payloadType, 0x0B) 51 | } 52 | 53 | if err := binary.Read(buf, binary.LittleEndian, &chunk.SampleRate); err != nil { 54 | return nil, err 55 | } 56 | 57 | var voiceType byte 58 | if err := binary.Read(buf, binary.LittleEndian, &voiceType); err != nil { 59 | return nil, err 60 | } 61 | 62 | if err := binary.Read(buf, binary.LittleEndian, &chunk.Length); err != nil { 63 | return nil, err 64 | } 65 | 66 | switch voiceType { 67 | case 0x6: 68 | remaining := buf.Len() 69 | chunkLen := int(chunk.Length) 70 | 71 | if remaining < chunkLen { 72 | return nil, fmt.Errorf("%w (received: %d bytes, expected at least %d bytes)", ErrInsufficientData, bLen, (bLen + (chunkLen - remaining))) 73 | } 74 | 75 | data := make([]byte, chunkLen) 76 | n, err := buf.Read(data) 77 | 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | // Is this even possible 83 | if n != chunkLen { 84 | return nil, fmt.Errorf("%w (expected to read %d bytes, but read %d bytes)", ErrInsufficientData, chunkLen, n) 85 | } 86 | 87 | chunk.Data = data 88 | case 0x0: 89 | // no-op, detect silence if chunk.Data is empty 90 | // the length would the number of silence frames 91 | default: 92 | return nil, fmt.Errorf("%w (expected 0x6 or 0x0 voice data, received %x)", ErrInvalidVoicePacket, voiceType) 93 | } 94 | 95 | remaining := buf.Len() 96 | 97 | if remaining != 4 { 98 | return nil, fmt.Errorf("%w (has %d bytes remaining, expected 4 bytes remaining)", ErrInvalidVoicePacket, remaining) 99 | } 100 | 101 | if err := binary.Read(buf, binary.LittleEndian, &chunk.Checksum); err != nil { 102 | return nil, err 103 | } 104 | 105 | actualChecksum := crc32.ChecksumIEEE(b[0 : bLen-4]) 106 | 107 | if chunk.Checksum != actualChecksum { 108 | return nil, fmt.Errorf("%w (received %x, expected %x)", ErrMismatchChecksum, chunk.Checksum, actualChecksum) 109 | } 110 | 111 | return chunk, nil 112 | } 113 | -------------------------------------------------------------------------------- /decoder/decoder.go: -------------------------------------------------------------------------------- 1 | package decoder 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "gopkg.in/hraban/opus.v2" 7 | ) 8 | 9 | const ( 10 | FrameSize = 480 11 | ) 12 | 13 | type OpusDecoder struct { 14 | decoder *opus.Decoder 15 | 16 | currentFrame uint16 17 | } 18 | 19 | func NewOpusDecoder(sampleRate, channels int) (*OpusDecoder, error) { 20 | decoder, err := opus.NewDecoder(sampleRate, channels) 21 | 22 | if err != nil { 23 | return nil, err 24 | } 25 | 26 | return &OpusDecoder{ 27 | decoder: decoder, 28 | currentFrame: 0, 29 | }, nil 30 | } 31 | 32 | func (d *OpusDecoder) Decode(b []byte) ([]float32, error) { 33 | buf := bytes.NewBuffer(b) 34 | 35 | output := make([]float32, 0, 1024) 36 | 37 | for buf.Len() != 0 { 38 | var chunkLen int16 39 | if err := binary.Read(buf, binary.LittleEndian, &chunkLen); err != nil { 40 | return nil, err 41 | } 42 | 43 | if chunkLen == -1 { 44 | d.currentFrame = 0 45 | break 46 | } 47 | 48 | var currentFrame uint16 49 | if err := binary.Read(buf, binary.LittleEndian, ¤tFrame); err != nil { 50 | return nil, err 51 | } 52 | 53 | previousFrame := d.currentFrame 54 | 55 | chunk := make([]byte, chunkLen) 56 | n, err := buf.Read(chunk) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if n != int(chunkLen) { 62 | return nil, ErrInvalidVoicePacket 63 | } 64 | 65 | if currentFrame >= previousFrame { 66 | if currentFrame == previousFrame { 67 | d.currentFrame = currentFrame + 1 68 | 69 | decoded, err := d.decodeSteamChunk(chunk) 70 | 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | output = append(output, decoded...) 76 | } else { 77 | decoded, err := d.decodeLoss(currentFrame - previousFrame) 78 | 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | output = append(output, decoded...) 84 | } 85 | } 86 | } 87 | 88 | return output, nil 89 | } 90 | 91 | func (d *OpusDecoder) decodeSteamChunk(b []byte) ([]float32, error) { 92 | o := make([]float32, FrameSize) 93 | 94 | n, err := d.decoder.DecodeFloat32(b, o) 95 | 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | return o[:n], nil 101 | } 102 | 103 | func (d *OpusDecoder) decodeLoss(samples uint16) ([]float32, error) { 104 | loss := min(samples, 10) 105 | 106 | o := make([]float32, 0, FrameSize*loss) 107 | 108 | for i := 0; i < int(loss); i += 1 { 109 | t := make([]float32, FrameSize) 110 | 111 | if err := d.decoder.DecodePLCFloat32(t); err != nil { 112 | return nil, err 113 | } 114 | 115 | o = append(o, t...) 116 | } 117 | 118 | return o, nil 119 | } 120 | 121 | func NewDecoder(sampleRate, channels int) (decoder *opus.Decoder, err error) { 122 | decoder, err = opus.NewDecoder(sampleRate, channels) 123 | return 124 | } 125 | 126 | func Decode(decoder *opus.Decoder, data []byte) (pcm []float32, err error) { 127 | pcm = make([]float32, 1024) 128 | 129 | nlen, err := decoder.DecodeFloat32(data, pcm) 130 | if err != nil { 131 | return 132 | } 133 | 134 | return pcm[:nlen], nil 135 | } 136 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module CS2VoiceData 2 | 3 | go 1.21 4 | 5 | toolchain go1.21.4 6 | 7 | require ( 8 | github.com/go-audio/audio v1.0.0 9 | github.com/go-audio/wav v1.1.0 10 | github.com/markus-wa/demoinfocs-golang/v4 v4.0.3 11 | gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 12 | ) 13 | 14 | require ( 15 | github.com/go-audio/riff v1.0.0 // indirect 16 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217 // indirect 17 | github.com/golang/snappy v0.0.4 // indirect 18 | github.com/markus-wa/go-unassert v0.1.3 // indirect 19 | github.com/markus-wa/gobitread v0.2.3 // indirect 20 | github.com/markus-wa/godispatch v1.4.1 // indirect 21 | github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 // indirect 22 | github.com/markus-wa/quickhull-go/v2 v2.2.0 // indirect 23 | github.com/oklog/ulid/v2 v2.1.0 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | google.golang.org/protobuf v1.32.0 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/go-audio/audio v1.0.0 h1:zS9vebldgbQqktK4H0lUqWrG8P0NxCJVqcj7ZpNnwd4= 4 | github.com/go-audio/audio v1.0.0/go.mod h1:6uAu0+H2lHkwdGsAY+j2wHPNPpPoeg5AaEFh9FlA+Zs= 5 | github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= 6 | github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498= 7 | github.com/go-audio/wav v1.1.0 h1:jQgLtbqBzY7G+BM8fXF7AHUk1uHUviWS4X39d5rsL2g= 8 | github.com/go-audio/wav v1.1.0/go.mod h1:mpe9qfwbScEbkd8uybLuIpTgHyrISw/OTuvjUW2iGtE= 9 | github.com/golang/geo v0.0.0-20180826223333-635502111454/go.mod h1:vgWZ7cu0fq0KY3PpEHsocXOWJpRtkcbKemU4IUw0M60= 10 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217 h1:HKlyj6in2JV6wVkmQ4XmG/EIm+SCYlPZ+V4GWit7Z+I= 11 | github.com/golang/geo v0.0.0-20230421003525-6adc56603217/go.mod h1:8wI0hitZ3a1IxZfeH3/5I97CI8i5cLGsYe7xNhQGs9U= 12 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 13 | github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 14 | github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 15 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 16 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 17 | github.com/markus-wa/demoinfocs-golang/v4 v4.0.0 h1:9ykwVs2uAN6Plydi4wD9ixL1gledE6kz3kX4zIZjRqY= 18 | github.com/markus-wa/demoinfocs-golang/v4 v4.0.0/go.mod h1:YuAfxa0q7mG+HSUjrSXFmaZ8xVRWK9k/vcG/rLbzrBA= 19 | github.com/markus-wa/demoinfocs-golang/v4 v4.0.3 h1:xs8FjO/o5z3bjEwTxfiGO4yK9bQESDawiFIgOmW6sRA= 20 | github.com/markus-wa/demoinfocs-golang/v4 v4.0.3/go.mod h1:kDkzriHU1eK8bjnL0QsSgPjkbNLlCPE+dfaYaneEJ5k= 21 | github.com/markus-wa/go-unassert v0.1.3 h1:4N2fPLUS3929Rmkv94jbWskjsLiyNT2yQpCulTFFWfM= 22 | github.com/markus-wa/go-unassert v0.1.3/go.mod h1:/pqt7a0LRmdsRNYQ2nU3SGrXfw3bLXrvIkakY/6jpPY= 23 | github.com/markus-wa/gobitread v0.2.3 h1:COx7dtYQ7Q+77hgUmD+O4MvOcqG7y17RP3Z7BbjRvPs= 24 | github.com/markus-wa/gobitread v0.2.3/go.mod h1:PcWXMH4gx7o2CKslbkFkLyJB/aHW7JVRG3MRZe3PINg= 25 | github.com/markus-wa/godispatch v1.4.1 h1:Cdff5x33ShuX3sDmUbYWejk7tOuoHErFYMhUc2h7sLc= 26 | github.com/markus-wa/godispatch v1.4.1/go.mod h1:tk8L0yzLO4oAcFwM2sABMge0HRDJMdE8E7xm4gK/+xM= 27 | github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7 h1:aR9pvnlnBxifXBmzidpAiq2prLSGlkhE904qnk2sCz4= 28 | github.com/markus-wa/ice-cipher-go v0.0.0-20230901094113-348096939ba7/go.mod h1:JIsht5Oa9P50VnGJTvH2a6nkOqDFJbUeU1YRZYvdplw= 29 | github.com/markus-wa/quickhull-go/v2 v2.2.0 h1:rB99NLYeUHoZQ/aNRcGOGqjNBGmrOaRxdtqTnsTUPTA= 30 | github.com/markus-wa/quickhull-go/v2 v2.2.0/go.mod h1:EuLMucfr4B+62eipXm335hOs23LTnO62W7Psn3qvU2k= 31 | github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= 32 | github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= 33 | github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= 34 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 35 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 36 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 37 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= 40 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 42 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 43 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 45 | google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= 46 | google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 47 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 48 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 49 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 50 | gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 h1:xeVptzkP8BuJhoIjNizd2bRHfq9KB9HfOLZu90T04XM= 51 | gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302/go.mod h1:/L5E7a21VWl8DeuCPKxQBdVG5cy+L0MRZ08B1wnqt7g= 52 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | --------------------------------------------------------------------------------