From 03b3691e590ca35aa3c3a35994fa803509cabe68 Mon Sep 17 00:00:00 2001 From: LaihoE <80683769+LaihoE@users.noreply.github.com> Date: Fri, 15 Mar 2024 13:06:02 +0200 Subject: [PATCH] fix audio (#148) --- src/parser/Cargo.toml | 1 + src/parser/src/first_pass/read_bits.rs | 2 + src/parser/src/second_pass/mod.rs | 1 + src/parser/src/second_pass/voice_data.rs | 117 +++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 src/parser/src/second_pass/voice_data.rs diff --git a/src/parser/Cargo.toml b/src/parser/Cargo.toml index 83209578..a52f5292 100644 --- a/src/parser/Cargo.toml +++ b/src/parser/Cargo.toml @@ -22,6 +22,7 @@ rayon = "1.7.0" protobuf-support = "3.3.0" proc-macro2 = "1.0.69" rand = "0.8.5" +opus = "0.3.0" [dependencies.csgoproto] path = "../csgoproto" diff --git a/src/parser/src/first_pass/read_bits.rs b/src/parser/src/first_pass/read_bits.rs index 28b6b857..cd66e6d8 100644 --- a/src/parser/src/first_pass/read_bits.rs +++ b/src/parser/src/first_pass/read_bits.rs @@ -244,6 +244,8 @@ pub enum DemoParserError { IllegalPathOp, VectorResizeFailure, ImpossibleCmd, + UnkVoiceFormat, + MalformedVoicePacket, } impl std::error::Error for DemoParserError {} diff --git a/src/parser/src/second_pass/mod.rs b/src/parser/src/second_pass/mod.rs index 2e1a751e..c81ce44f 100644 --- a/src/parser/src/second_pass/mod.rs +++ b/src/parser/src/second_pass/mod.rs @@ -7,3 +7,4 @@ pub mod parser; pub mod parser_settings; pub mod path_ops; pub mod variants; +pub mod voice_data; diff --git a/src/parser/src/second_pass/voice_data.rs b/src/parser/src/second_pass/voice_data.rs new file mode 100644 index 00000000..70ceb502 --- /dev/null +++ b/src/parser/src/second_pass/voice_data.rs @@ -0,0 +1,117 @@ +use crate::first_pass::read_bits::DemoParserError; +use ahash::AHashMap; +use csgoproto::netmessages::CSVCMsg_VoiceData; +use opus::Decoder; +use rayon::iter::IntoParallelRefIterator; +use rayon::iter::ParallelIterator; +use std::{i16, time::Instant}; + +#[derive(Debug)] +struct VoicePacket { + pub length: u16, + pub voice_type: u8, +} +const FRAME_SIZE: usize = 480; +const AVG_BYTES_PER_PACKET: usize = 1600; + +pub fn parse_voice_chunk_old_format(bytes: &[u8], decoder: &mut Decoder) -> Result, DemoParserError> { + // based on https://github.com/DandrewsDev/CS2VoiceData + let mut decoded_bytes = vec![]; + let packet = VoicePacket { + // sample_rate: u16::from_le_bytes(bytes[9..11].try_into().unwrap()), + voice_type: u8::from_le_bytes([bytes[11]].try_into().unwrap()), + length: u16::from_le_bytes(bytes[12..14].try_into().unwrap()), + }; + if packet.voice_type == 6 { + let mut ptr = 14; + + // read chunks until chunk_len == 65535 + while ptr < packet.length as usize { + let mut output = vec![0; FRAME_SIZE]; + let chunk_len = u16::from_le_bytes(bytes[ptr..ptr + 2].try_into().unwrap()); + if chunk_len == 65535 { + break; + } + ptr += 4; + match decoder.decode(&bytes, &mut decoded_bytes, false) { + Ok(n) => decoded_bytes.extend(&output[..n]), + Err(_) => return Err(DemoParserError::MalformedVoicePacket), + }; + ptr += chunk_len as usize; + } + } + Ok(decoded_bytes) +} +pub fn parse_voice_chunk_new_format(bytes: &[u8], decoder: &mut Decoder) -> Result, DemoParserError> { + let mut decoded_bytes = vec![0; 1024]; + let n = match decoder.decode(&bytes, &mut decoded_bytes, false) { + Ok(n) => n, + Err(_) => return Err(DemoParserError::MalformedVoicePacket), + }; + decoded_bytes.truncate(n); + Ok(decoded_bytes) +} + +fn generate_wav_header(num_channels: u16, sample_rate: u32, bits_per_sample: u16, data_size: u32) -> Vec { + let mut header = Vec::new(); + // RIFF header + header.extend_from_slice(b"RIFF"); + header.extend_from_slice(&((36 + data_size) as u32).to_le_bytes()); + header.extend_from_slice(b"WAVE"); + // Format chunk + header.extend_from_slice(b"fmt "); + header.extend_from_slice(&(16 as u32).to_le_bytes()); + header.extend_from_slice(&(1 as u16).to_le_bytes()); + header.extend_from_slice(&num_channels.to_le_bytes()); + header.extend_from_slice(&sample_rate.to_le_bytes()); + header.extend_from_slice(&(sample_rate * num_channels as u32 * bits_per_sample as u32 / 8).to_le_bytes()); + header.extend_from_slice(&(num_channels * bits_per_sample / 8).to_le_bytes()); + header.extend_from_slice(&bits_per_sample.to_le_bytes()); + // Data chunk + header.extend_from_slice(b"data"); + header.extend_from_slice(&data_size.to_le_bytes()); + header +} +pub fn convert_voice_data_to_wav(voice_data: Vec) -> Result)>, DemoParserError> { + // Group by steamid + let mut hm: AHashMap> = AHashMap::default(); + for data in &voice_data { + hm.entry(data.xuid()).or_insert(vec![]).push(data); + } + // Collect voice data per steamid + let voice_data_wav: Vec), DemoParserError>> = hm + .iter() + .map(|(xuid, data)| { + let mut decoder = Decoder::new(48000, opus::Channels::Mono).unwrap(); + let mut data_this_player = Vec::with_capacity(AVG_BYTES_PER_PACKET * data.len()); + // add header + data_this_player.extend(generate_wav_header(1, 48000, 16, data_this_player.len() as u32)); + // add voice data + for chunk in data { + match chunk.audio.format() { + csgoproto::netmessages::VoiceDataFormat_t::VOICEDATA_FORMAT_OPUS => data_this_player.extend( + parse_voice_chunk_new_format(chunk.audio.voice_data(), &mut decoder)? + .iter() + .flat_map(|x| x.to_le_bytes()), + ), + csgoproto::netmessages::VoiceDataFormat_t::VOICEDATA_FORMAT_STEAM => data_this_player.extend( + parse_voice_chunk_new_format(chunk.audio.voice_data(), &mut decoder)? + .iter() + .flat_map(|x| x.to_le_bytes()), + ), + csgoproto::netmessages::VoiceDataFormat_t::VOICEDATA_FORMAT_ENGINE => { + return Err(DemoParserError::UnkVoiceFormat); + } + }; + } + Ok((xuid.to_string(), data_this_player)) + }) + .collect(); + + // Check for errors + let mut ok_packets = vec![]; + for data in voice_data_wav { + ok_packets.push(data?); + } + Ok(ok_packets) +}