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

Add multiple voice to wav option #249

Open
dayzmarine opened this issue Feb 17, 2025 · 3 comments
Open

Add multiple voice to wav option #249

dayzmarine opened this issue Feb 17, 2025 · 3 comments

Comments

@dayzmarine
Copy link

Hi just wondering if you could add maybe better option for voice to wav.

  1. Have each communication output as a wav file (for example Player3 presses the mic then stops, make that a .wav the moment he stops) (for voice analysis with LLM for exemple
  2. Have the voice structured such as in a match, because right now the voice sequence are all mashed together so voice comm are not reflective of actual ingame performance.

IF u dont understand well let me know, english not my first language
thx much!

@dayzmarine
Copy link
Author

@LaihoE

@LaihoE
Copy link
Owner

LaihoE commented Feb 19, 2025

Yeah this has been suggested before and could be great. It will just require some work.

@emilovz
Copy link

emilovz commented Mar 5, 2025

To give you an idea, got it to work for python with help from AI, I use it like this:

parser = DemoParser(file)
voice_data = parser.parse_voice()
for steamid, segments in voice_data.items():
    for segment in segments:
        starting_tick = segment['start_tick']
        audio_bytes = segment["audio"]
# etc

with these modifications:
File: \src\python\src\lib.rs

        let voice_segments = convert_voice_data_to_wav(output.voice_data).map_err(|e| PyValueError::new_err(format!("{e}")))?;
        let mut out_hm = AHashMap::new();
        for (steamid, segments) in voice_segments {
            let py_segments = PyList::new_bound(py, segments.into_iter().map(|(start_tick, bytes)| {
                let py_dict = PyDict::new_bound(py);
                py_dict.set_item("start_tick", start_tick).unwrap();
                py_dict.set_item("audio", PyBytes::new_bound(py, &bytes)).unwrap();
                py_dict
            }));
            out_hm.insert(steamid, py_segments);

instead of

        let out = convert_voice_data_to_wav(output.voice_data).unwrap();
        let mut out_hm = AHashMap::default();
        for (steamid, bytes) in out {
            let py_bytes = PyBytes::new_bound(py, &bytes);
            out_hm.insert(steamid, py_bytes);

File: \src\parser\src\second_pass\voice_data.rs

pub fn convert_voice_data_to_wav(voice_data: Vec<(i32, CSVCMsg_VoiceData)>) -> Result<Vec<(String, Vec<(i32, Vec<u8>)>)>, DemoParserError> {
    // Group voice data by SteamID
    let mut hm: AHashMap<u64, Vec<(i32, &CSVCMsg_VoiceData)>> = AHashMap::default();
    for (tick, data) in &voice_data {
        hm.entry(data.xuid()).or_insert_with(Vec::new).push((*tick, data));
    }
    // Process each player's data in parallel
    let voice_data_wav: Vec<_> = hm.par_iter().map(|(xuid, data)| {
        let mut decoder = Decoder::new(48000, opus::Channels::Mono).unwrap();
        let mut segments = Vec::new();
        let mut current_segment = Vec::new();
        let mut segment_start_tick = None;
        let mut last_tick = None;
        const GAP_THRESHOLD: i32 = 32; // ~500ms at 64 ticks/sec
        // Sort by tick to ensure chronological order (if not guaranteed by parser)
        let mut sorted_data = data.to_vec();
        sorted_data.sort_by_key(|&(tick, _)| tick);
        for (tick, chunk) in sorted_data {
            // Check for gap to start new segment
            if let Some(lt) = last_tick {
                if tick - lt > GAP_THRESHOLD {
                    if !current_segment.is_empty() {
                        segments.push((segment_start_tick.unwrap(), current_segment));
                        current_segment = Vec::new();
                    }
                    segment_start_tick = Some(tick);
                }
            } else {
                segment_start_tick = Some(tick); // First chunk in segment
            }
            // Decode voice chunk
            let pcm = match chunk.audio.format() {
                csgoproto::netmessages::VoiceDataFormat_t::VOICEDATA_FORMAT_OPUS => {
                    parse_voice_chunk_new_format(chunk.audio.voice_data(), &mut decoder)?
                }
                csgoproto::netmessages::VoiceDataFormat_t::VOICEDATA_FORMAT_STEAM => {
                    parse_voice_chunk_old_format(chunk.audio.voice_data(), &mut decoder)?
                }
                _ => return Err(DemoParserError::UnkVoiceFormat),
            };
            // Convert PCM samples to bytes
            let pcm_bytes = pcm.iter().flat_map(|x| x.to_le_bytes()).collect::<Vec<u8>>();
            current_segment.extend(pcm_bytes);
            last_tick = Some(tick);
        }
        // Push the last segment if it exists
        if !current_segment.is_empty() {
            segments.push((segment_start_tick.unwrap(), current_segment));
        }
        // Convert segments to WAV files
        let wav_files: Vec<(i32, Vec<u8>)> = segments.into_iter().map(|(start_tick, bytes)| {
            let mut out = Vec::new();
            out.extend(generate_wav_header(1, 48000, 16, bytes.len() as u32)); // Mono, 48kHz, 16-bit
            out.extend(bytes);
            (start_tick, out)
        }).collect();
        Ok((xuid.to_string(), wav_files))
    }).collect();
    // Collect results, propagating errors
    let mut ok_packets = Vec::new();

instead of

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
        .par_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 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);
                    }
                };
            }
            let mut out = vec![];
            out.extend(generate_wav_header(1, 48000, 16, data_this_player.len() as u32));
            out.extend(data_this_player);
            Ok((xuid.to_string(), out))
        })
        .collect();
    // Check for errors
    let mut ok_packets = vec![];

File: \src\parser\src\second_pass\parser_settings.rs

pub voice_data: Vec<(i32, CSVCMsg_VoiceData)>,

instead of

pub voice_data: Vec<CSVCMsg_VoiceData>,

File: \src\parser\src\second_pass\parser.rs

pub voice_data: Vec<(i32, CSVCMsg_VoiceData)>,

instead of

pub voice_data: Vec<CSVCMsg_VoiceData>,

and

self.voice_data.push((self.tick, m));

instead of

self.voice_data.push(m);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants