#[cfg(test)]
#[path = "../tests/net/steam_voice.rs"]
mod tests;
const STEAM_ID_BYTES: usize = 8;
const CRC_BYTES: usize = 4;
const SILENCE: u8 = 0;
const OPUS_PLC: u8 = 6;
const SAMPLE_RATE: u8 = 11;
const RESET: u16 = u16::MAX;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum SteamVoiceError {
#[error("the packet is {len} bytes long, too short for a Steam ID and a CRC")]
TooShort {
len: usize,
},
#[error("the packet's CRC is {stored:#010x}, but its contents' is {computed:#010x}")]
Crc {
stored: u32,
computed: u32,
},
#[error("a section has the unknown type {kind}")]
UnknownSection {
kind: u8,
},
#[error("a section or frame is cut short")]
Truncated,
#[error("an Opus section or frame of {len} bytes is too long to encode")]
TooLong {
len: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Section<'a> {
Silence(u16),
SampleRate(u16),
Opus(&'a [u8]),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OpusFrame<'a> {
Audio {
sequence: u16,
data: &'a [u8],
},
Reset {
sequence: u16,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SteamVoice<'a> {
steam_id: u64,
sections: &'a [u8],
}
impl<'a> SteamVoice<'a> {
pub fn parse(packet: &'a [u8]) -> Result<Self, SteamVoiceError> {
let len = packet.len();
let Some(contents_len) = len
.checked_sub(CRC_BYTES)
.filter(|&contents| contents >= STEAM_ID_BYTES)
else {
return Err(SteamVoiceError::TooShort { len });
};
let (contents, stored) = packet.split_at(contents_len);
let stored = u32::from_le_bytes(stored.try_into().unwrap());
let computed = crc32(contents);
if stored != computed {
return Err(SteamVoiceError::Crc { stored, computed });
}
let (steam_id, sections) = contents.split_at(STEAM_ID_BYTES);
Ok(Self {
steam_id: u64::from_le_bytes(steam_id.try_into().unwrap()),
sections,
})
}
pub fn sections(self) -> impl Iterator<Item = Result<Section<'a>, SteamVoiceError>> {
let mut rest = self.sections;
std::iter::from_fn(move || {
let (&kind, after) = rest.split_first()?;
let section = read_u16(after).and_then(|(value, after)| {
let (section, after) = match kind {
SILENCE => (Section::Silence(value), after),
SAMPLE_RATE => (Section::SampleRate(value), after),
OPUS_PLC => {
let frames = after
.get(..value as usize)
.ok_or(SteamVoiceError::Truncated)?;
(Section::Opus(frames), &after[frames.len()..])
}
kind => return Err(SteamVoiceError::UnknownSection { kind }),
};
rest = after;
Ok(section)
});
if section.is_err() {
rest = &[];
}
Some(section)
})
}
pub const fn steam_id(self) -> u64 {
self.steam_id
}
pub fn with_steam_id(self, steam_id: u64) -> Vec<u8> {
packet(steam_id, self.sections)
}
}
pub fn crc32(bytes: &[u8]) -> u32 {
const POLYNOMIAL: u32 = 0xEDB8_8320;
let crc = bytes.iter().fold(u32::MAX, |crc, &byte| {
(0..8).fold(crc ^ u32::from(byte), |crc, _| match crc & 1 {
0 => crc >> 1,
_ => (crc >> 1) ^ POLYNOMIAL,
})
});
!crc
}
pub fn encode(steam_id: u64, sections: &[Section<'_>]) -> Result<Vec<u8>, SteamVoiceError> {
let mut body = Vec::new();
for section in sections {
let (kind, value, data) = match *section {
Section::Silence(samples) => (SILENCE, samples, &[][..]),
Section::SampleRate(rate) => (SAMPLE_RATE, rate, &[][..]),
Section::Opus(frames) => {
let len = u16::try_from(frames.len())
.map_err(|_| SteamVoiceError::TooLong { len: frames.len() })?;
(OPUS_PLC, len, frames)
}
};
body.push(kind);
body.extend_from_slice(&value.to_le_bytes());
body.extend_from_slice(data);
}
Ok(packet(steam_id, &body))
}
pub fn encode_opus_frames(frames: &[OpusFrame<'_>]) -> Result<Vec<u8>, SteamVoiceError> {
let mut out = Vec::new();
for frame in frames {
let (len, sequence, data) = match *frame {
OpusFrame::Reset { sequence } => (RESET, sequence, &[][..]),
OpusFrame::Audio { sequence, data } => {
let len = u16::try_from(data.len())
.ok()
.filter(|&len| len != RESET)
.ok_or(SteamVoiceError::TooLong { len: data.len() })?;
(len, sequence, data)
}
};
out.extend_from_slice(&len.to_le_bytes());
out.extend_from_slice(&sequence.to_le_bytes());
out.extend_from_slice(data);
}
Ok(out)
}
pub fn opus_frames(
mut frames: &[u8],
) -> impl Iterator<Item = Result<OpusFrame<'_>, SteamVoiceError>> {
std::iter::from_fn(move || {
if frames.is_empty() {
return None;
}
let frame = read_u16(frames).and_then(|(len, after)| {
let (sequence, after) = read_u16(after)?;
if len == RESET {
frames = after;
return Ok(OpusFrame::Reset { sequence });
}
let data = after
.get(..len as usize)
.ok_or(SteamVoiceError::Truncated)?;
frames = &after[data.len()..];
Ok(OpusFrame::Audio { sequence, data })
});
if frame.is_err() {
frames = &[];
}
Some(frame)
})
}
fn packet(steam_id: u64, body: &[u8]) -> Vec<u8> {
let mut packet = Vec::with_capacity(STEAM_ID_BYTES + body.len() + CRC_BYTES);
packet.extend_from_slice(&steam_id.to_le_bytes());
packet.extend_from_slice(body);
packet.extend_from_slice(&crc32(&packet).to_le_bytes());
packet
}
fn read_u16(bytes: &[u8]) -> Result<(u16, &[u8]), SteamVoiceError> {
let (value, rest) = bytes
.split_first_chunk::<2>()
.ok_or(SteamVoiceError::Truncated)?;
Ok((u16::from_le_bytes(*value), rest))
}