1
0
Fork 0
mirror of https://codeberg.org/demostf/parser.git synced 2026-06-03 10:14:06 +02:00
This commit is contained in:
Robin Appelman 2025-04-29 20:56:48 +02:00
commit 1fed4d8826
6 changed files with 247 additions and 25 deletions

57
Cargo.lock generated
View file

@ -44,6 +44,17 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "audiopus_sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651"
dependencies = [
"cmake",
"log",
"pkg-config",
]
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.3.0" version = "1.3.0"
@ -122,9 +133,12 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.99" version = "1.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96c51067fd44124faa7f870b4b1c969379ad32b2ba805aa959430ceaa384f695" checksum = "04da6a0d40b948dfc4fa8f5bbf402b0fc1a64a28dbf7d12ffd683550f2c1b63a"
dependencies = [
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -184,6 +198,15 @@ version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70" checksum = "4b82cf0babdbd58558212896d1a4272303a57bdb245c2bf1147185fb45640e70"
[[package]]
name = "cmake"
version = "0.1.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "console" name = "console"
version = "0.15.8" version = "0.15.8"
@ -373,6 +396,12 @@ version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
[[package]]
name = "hound"
version = "3.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f"
[[package]] [[package]]
name = "iai" name = "iai"
version = "0.1.1" version = "0.1.1"
@ -695,6 +724,16 @@ version = "11.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575"
[[package]]
name = "opus"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e"
dependencies = [
"audiopus_sys",
"libc",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -732,6 +771,12 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]] [[package]]
name = "plotters" name = "plotters"
version = "0.3.6" version = "0.3.6"
@ -1015,6 +1060,12 @@ dependencies = [
"lazy_static", "lazy_static",
] ]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "similar" name = "similar"
version = "2.5.0" version = "2.5.0"
@ -1148,6 +1199,7 @@ dependencies = [
"criterion", "criterion",
"enumflags2", "enumflags2",
"fnv", "fnv",
"hound",
"iai", "iai",
"insta", "insta",
"itertools 0.13.0", "itertools 0.13.0",
@ -1157,6 +1209,7 @@ dependencies = [
"no-panic", "no-panic",
"num-traits 0.2.19", "num-traits 0.2.19",
"num_enum", "num_enum",
"opus",
"parse-display", "parse-display",
"pretty_assertions", "pretty_assertions",
"prettyplease", "prettyplease",

View file

@ -44,6 +44,11 @@ path = "src/bin/strings.rs"
name = "direct_hits" name = "direct_hits"
path = "src/bin/direct_hits.rs" path = "src/bin/direct_hits.rs"
[[bin]]
name = "voice"
path = "src/bin/voice.rs"
required-features = ["audio"]
[dependencies] [dependencies]
bitbuffer = { version = "0.11.0", features = ["serde"] } bitbuffer = { version = "0.11.0", features = ["serde"] }
num_enum = "0.7.2" num_enum = "0.7.2"
@ -77,10 +82,15 @@ tempfile = { version = "3", optional = true }
lazy_static = { version = "1", optional = true } lazy_static = { version = "1", optional = true }
prettyplease = { version = "0.2", optional = true } prettyplease = { version = "0.2", optional = true }
# audio
opus = { version = "0.3.0", optional = true }
hound = { version = "3.5.1", optional = true }
[features] [features]
schema = ["schemars", "bitbuffer/schemars"] schema = ["schemars", "bitbuffer/schemars"]
trace = ["tracing", "tracing-subscriber"] trace = ["tracing", "tracing-subscriber"]
codegen = ["better-panic", "quote", "syn", "Inflector", "proc-macro2", "tempfile", "lazy_static", "prettyplease"] codegen = ["better-panic", "quote", "syn", "Inflector", "proc-macro2", "tempfile", "lazy_static", "prettyplease"]
audio = ["dep:opus", "dep:hound"]
[dev-dependencies] [dev-dependencies]
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"

27
flake.lock generated
View file

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"crane": { "crane": {
"locked": { "locked": {
"lastModified": 1733688869, "lastModified": 1742394900,
"narHash": "sha256-KrhxxFj1CjESDrL5+u/zsVH0K+Ik9tvoac/oFPoxSB8=", "narHash": "sha256-vVOAp9ahvnU+fQoKd4SEXB2JG2wbENkpqcwlkIXgUC0=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "604637106e420ad99907cae401e13ab6b452e7d9", "rev": "70947c1908108c0c551ddfd73d4f750ff2ea67cd",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -44,17 +44,14 @@
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
}, },
"locked": { "locked": {
"lastModified": 1735052218, "lastModified": 1745880182,
"narHash": "sha256-I30wh6G8fSUO4EseexxiDXcxyUhXR6C8BvEeKn6xyfE=", "narHash": "sha256-QEhBdGhuVArNIVUfblAbA115qXulEJWdR465gqIKF2A=",
"owner": "icewind1991", "path": "/home/robin/Projects/mill-scale",
"repo": "mill-scale", "type": "path"
"rev": "7e45bb598ff63a8416ee3c26743b20644563bd93",
"type": "github"
}, },
"original": { "original": {
"owner": "icewind1991", "path": "/home/robin/Projects/mill-scale",
"repo": "mill-scale", "type": "path"
"type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
@ -88,11 +85,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1733884434, "lastModified": 1742697269,
"narHash": "sha256-8GXR9kC07dyOIshAyfZhG11xfvBRSZzYghnZ2weOKJU=", "narHash": "sha256-Lpp0XyAtIl1oGJzNmTiTGLhTkcUjwSkEb0gOiNzYFGM=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "d0483df44ddf0fd1985f564abccbe568e020ddf2", "rev": "01973c84732f9275c50c5f075dd1f54cc04b3316",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -6,7 +6,8 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
mill-scale = { mill-scale = {
url = "github:icewind1991/mill-scale"; # url = "github:icewind1991/mill-scale";
url = "path:/home/robin/Projects/mill-scale";
inputs.flakelight.follows = "flakelight"; inputs.flakelight.follows = "flakelight";
}; };
}; };

161
src/bin/voice.rs Normal file
View file

@ -0,0 +1,161 @@
use std::env;
use std::fs;
use bitbuffer::{BigEndian, BitRead, BitReadStream, LittleEndian};
use hound::{SampleFormat, WavSpec, WavWriter};
use main_error::MainError;
use opus::{Channels, Decoder};
use opus::packet::parse;
use steamid_ng::SteamID;
use tf_demo_parser::demo::parser::MessageHandler;
use tf_demo_parser::MessageType;
pub use tf_demo_parser::{Demo, DemoParser, Parse, ParserState};
use tf_demo_parser::demo::data::DemoTick;
use tf_demo_parser::demo::message::Message;
use tf_demo_parser::demo::message::voice::{VoiceInitMessage};
#[cfg(feature = "jemallocator")]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
fn main() -> Result<(), MainError> {
#[cfg(feature = "better-panic")]
better_panic::install();
#[cfg(feature = "trace")]
tracing_subscriber::fmt::init();
let args: Vec<_> = env::args().collect();
if args.len() < 2 {
println!("1 argument required");
return Ok(());
}
let path = args[1].clone();
let file = fs::read(path)?;
let demo = Demo::new(&file);
let parser = DemoParser::new_with_analyser(demo.get_stream(), Voice::default());
let (_header, samples) = parser.parse()?;
let mut sample_buf = vec![0; 16 * 1024];
let spec = WavSpec {
channels: 1,
sample_rate: 44100,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut writer = WavWriter::create("out.wav", spec).unwrap();
for sample in samples {
let packet = decode_steam_voice_sample(&sample.data);
let mut decoder = Decoder::new(packet.sample_rate as u32, Channels::Mono).unwrap();
dbg!(&packet.data[0..8]);
dbg!(parse(&packet.data));
let samples = decoder.decode(&packet.data, &mut sample_buf, false).unwrap();
dbg!(samples);
for sample in &sample_buf[0..samples] {
writer.write_sample(*sample).unwrap();
}
}
Ok(())
}
#[derive(Default)]
struct Voice {
pub samples: Vec<VoiceSample>,
pub last_init: Option<VoiceInitMessage>,
}
#[derive(Debug)]
pub enum VoiceCodec {
Steam,
Speex,
Celt,
CeltHigh
}
#[derive(Debug)]
struct VoiceSample {
tick: DemoTick,
codec: VoiceCodec,
sampling_rate: u16,
client: u8,
data: Vec<u8>,
}
impl MessageHandler for Voice {
type Output = Vec<VoiceSample>;
fn does_handle(message_type: MessageType) -> bool {
matches!(
message_type,
MessageType::VoiceInit | MessageType::VoiceData
)
}
fn handle_message(&mut self, message: &Message, tick: DemoTick, _parser_state: &ParserState) {
match message {
Message::VoiceInit(init) => {
self.last_init = Some(init.clone());
}
Message::VoiceData(data) => {
if let Some(init) = self.last_init.as_ref() {
let codec = match init.codec.as_str() {
"steam" => VoiceCodec::Steam,
"vaudio_speex" => VoiceCodec::Speex,
"vaudio_celt" => VoiceCodec::Celt,
"vaudio_celt_high" => VoiceCodec::CeltHigh,
_ => {
eprintln!("unknown voice codex: {}", init.codec);
return;
}
};
self.samples.push(VoiceSample {
tick,
codec,
sampling_rate: init.sampling_rate,
data: data.data.clone().read_sized::<Vec<u8>>(data.length as usize / 8).unwrap(),
client: data.client,
})
} else {
eprintln!("Voice data without init");
}
}
_ => {}
}
}
fn into_output(self, _state: &ParserState) -> Self::Output {
self.samples
}
}
#[derive(BitRead, Debug)]
struct SteamVoiceHeader {
steam_id: u64,
ty: u8,
sample_rate: u16,
payload_type: u8,
length: u16,
}
#[derive(Debug)]
struct SteamVoicePacket {
steam_id: SteamID,
sample_rate: u16,
data: Vec<u8>,
}
fn decode_steam_voice_sample(data: &[u8]) -> SteamVoicePacket {
let mut reader = BitReadStream::<LittleEndian>::from(data);
let header = reader.read::<SteamVoiceHeader>().unwrap();
assert_eq!(header.ty, 11);
let data: Vec<u8> = if header.payload_type == 6 {
reader.read_sized(header.length as usize).unwrap()
} else {
dbg!(header.length, data.len());
Vec::new()
};
SteamVoicePacket {
steam_id: SteamID::from(header.steam_id),
sample_rate: header.sample_rate,
data,
}
}

View file

@ -6,9 +6,9 @@ use crate::{ReadResult, Stream};
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VoiceInitMessage { pub struct VoiceInitMessage {
codec: String, pub codec: String,
quality: u8, pub quality: u8,
sampling_rate: u16, pub sampling_rate: u16,
} }
impl BitRead<'_, LittleEndian> for VoiceInitMessage { impl BitRead<'_, LittleEndian> for VoiceInitMessage {
@ -67,11 +67,11 @@ fn test_voice_init_roundtrip() {
#[endianness = "LittleEndian"] #[endianness = "LittleEndian"]
#[serde(bound(deserialize = "'a: 'static"))] #[serde(bound(deserialize = "'a: 'static"))]
pub struct VoiceDataMessage<'a> { pub struct VoiceDataMessage<'a> {
client: u8, pub client: u8,
proximity: u8, pub proximity: u8,
length: u16, pub length: u16,
#[size = "length"] #[size = "length"]
data: Stream<'a>, pub data: Stream<'a>,
} }
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]