Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix audio #148

Merged
merged 1 commit into from
Mar 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/parser/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions src/parser/src/first_pass/read_bits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,8 @@ pub enum DemoParserError {
IllegalPathOp,
VectorResizeFailure,
ImpossibleCmd,
UnkVoiceFormat,
MalformedVoicePacket,
}

impl std::error::Error for DemoParserError {}
Expand Down
1 change: 1 addition & 0 deletions src/parser/src/second_pass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ pub mod parser;
pub mod parser_settings;
pub mod path_ops;
pub mod variants;
pub mod voice_data;
117 changes: 117 additions & 0 deletions src/parser/src/second_pass/voice_data.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<i16>, 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<Vec<i16>, 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<u8> {
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<CSVCMsg_VoiceData>) -> Result<Vec<(String, Vec<u8>)>, DemoParserError> {
// Group by steamid
let mut hm: AHashMap<u64, Vec<&CSVCMsg_VoiceData>> = 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<Result<(String, Vec<u8>), 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)
}
Loading