cat / goq
clone

turns a linux box into a private steam streaming appliance: plug in, tap your key, play from another computer

fix/hero-release-note
goq / crates / sigil-protocol / src / audio.rs
audio.rs 11.0K
1use crate::{2 MAX_AUDIO_PAYLOAD_LEN, OPUS_CHANNELS, OPUS_FRAME_SAMPLES, OPUS_SAMPLE_RATE, PROTOCOL_VERSION,3 ProtocolError, Result,4};56const AUDIO_MAGIC: [u8; 4] = *b"SGA1";7pub const AUDIO_HEADER_LEN: usize = 48;8const AUDIO_HEADER_LEN_U8: u8 = AUDIO_HEADER_LEN as u8;910#[derive(Debug, Clone, Copy, PartialEq, Eq)]11#[repr(u8)]12pub enum AudioCodec {13 Opus = 1,14}1516impl TryFrom<u8> for AudioCodec {17 type Error = ProtocolError;1819 fn try_from(value: u8) -> Result<Self> {20 match value {21 1 => Ok(Self::Opus),22 other => Err(ProtocolError::UnsupportedAudioCodec(other)),23 }24 }25}2627#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]28pub struct AudioFlags(u8);2930impl AudioFlags {31 pub const NONE: Self = Self(0);32 pub const DISCONTINUITY: Self = Self(1 << 0);33 const KNOWN: u8 = Self::DISCONTINUITY.0;3435 pub const fn from_bits(bits: u8) -> Option<Self> {36 if bits & !Self::KNOWN == 0 {37 Some(Self(bits))38 } else {39 None40 }41 }4243 pub const fn bits(self) -> u8 {44 self.045 }4647 pub const fn contains(self, flag: Self) -> bool {48 self.0 & flag.0 == flag.049 }50}5152/// Fixed-width header for one Opus packet carried in one QUIC datagram.53/// Integer fields use network byte order. `capture_timestamp_us` and `pts_us`54/// use the same host-monotonic media-session epoch as video timestamps.55#[derive(Debug, Clone, Copy, PartialEq, Eq)]56pub struct AudioPacketHeader {57 pub codec: AudioCodec,58 pub flags: AudioFlags,59 pub channels: u8,60 pub sample_rate: u32,61 pub frame_samples: u16,62 pub payload_len: u32,63 pub sequence: u64,64 pub capture_timestamp_us: u64,65 pub pts_us: i64,66}6768impl AudioPacketHeader {69 pub fn opus(70 payload_len: usize,71 sequence: u64,72 capture_timestamp_us: u64,73 pts_us: i64,74 flags: AudioFlags,75 ) -> Result<Self> {76 let payload_len =77 u32::try_from(payload_len).map_err(|_| ProtocolError::InvalidAudioPayloadLength {78 actual: payload_len,79 maximum: MAX_AUDIO_PAYLOAD_LEN,80 })?;81 let header = Self {82 codec: AudioCodec::Opus,83 flags,84 channels: OPUS_CHANNELS,85 sample_rate: OPUS_SAMPLE_RATE,86 frame_samples: OPUS_FRAME_SAMPLES,87 payload_len,88 sequence,89 capture_timestamp_us,90 pts_us,91 };92 header.validate()?;93 Ok(header)94 }9596 pub fn validate(&self) -> Result<()> {97 if self.channels != OPUS_CHANNELS98 || self.sample_rate != OPUS_SAMPLE_RATE99 || self.frame_samples != OPUS_FRAME_SAMPLES100 {101 return Err(ProtocolError::InvalidMessage {102 message_type: "audio header",103 reason: "v1 requires 48 kHz stereo Opus with 960 samples per packet",104 });105 }106 let payload_len = self.payload_len as usize;107 if payload_len == 0 || payload_len > MAX_AUDIO_PAYLOAD_LEN {108 return Err(ProtocolError::InvalidAudioPayloadLength {109 actual: payload_len,110 maximum: MAX_AUDIO_PAYLOAD_LEN,111 });112 }113 if AudioFlags::from_bits(self.flags.bits()).is_none() {114 return Err(ProtocolError::UnsupportedFlags(self.flags.bits()));115 }116 Ok(())117 }118119 pub fn encode(&self) -> Result<[u8; AUDIO_HEADER_LEN]> {120 self.validate()?;121 let mut wire = [0; AUDIO_HEADER_LEN];122 wire[0..4].copy_from_slice(&AUDIO_MAGIC);123 wire[4..6].copy_from_slice(&PROTOCOL_VERSION.to_be_bytes());124 wire[6] = AUDIO_HEADER_LEN_U8;125 wire[7] = self.codec as u8;126 wire[8] = self.flags.bits();127 wire[9] = self.channels;128 // Bytes 10..12 and 18..20 are reserved and remain zero.129 wire[12..16].copy_from_slice(&self.sample_rate.to_be_bytes());130 wire[16..18].copy_from_slice(&self.frame_samples.to_be_bytes());131 wire[20..24].copy_from_slice(&self.payload_len.to_be_bytes());132 wire[24..32].copy_from_slice(&self.sequence.to_be_bytes());133 wire[32..40].copy_from_slice(&self.capture_timestamp_us.to_be_bytes());134 wire[40..48].copy_from_slice(&self.pts_us.to_be_bytes());135 Ok(wire)136 }137138 pub fn decode(wire: &[u8; AUDIO_HEADER_LEN]) -> Result<Self> {139 let magic: [u8; 4] = wire[0..4].try_into().expect("fixed slice length");140 if magic != AUDIO_MAGIC {141 return Err(ProtocolError::InvalidAudioMagic(magic));142 }143 let version = u16::from_be_bytes(wire[4..6].try_into().expect("fixed slice length"));144 if version != PROTOCOL_VERSION {145 return Err(ProtocolError::UnsupportedVersion {146 expected: PROTOCOL_VERSION,147 actual: version,148 });149 }150 if wire[6] != AUDIO_HEADER_LEN_U8 {151 return Err(ProtocolError::InvalidHeaderLength {152 expected: AUDIO_HEADER_LEN_U8,153 actual: wire[6],154 });155 }156 if wire[10..12].iter().chain(&wire[18..20]).any(|b| *b != 0) {157 return Err(ProtocolError::InvalidMessage {158 message_type: "audio header",159 reason: "reserved fields must be zero",160 });161 }162 let flags =163 AudioFlags::from_bits(wire[8]).ok_or(ProtocolError::UnsupportedFlags(wire[8]))?;164 let header = Self {165 codec: AudioCodec::try_from(wire[7])?,166 flags,167 channels: wire[9],168 sample_rate: u32::from_be_bytes(wire[12..16].try_into().expect("fixed slice length")),169 frame_samples: u16::from_be_bytes(wire[16..18].try_into().expect("fixed slice length")),170 payload_len: u32::from_be_bytes(wire[20..24].try_into().expect("fixed slice length")),171 sequence: u64::from_be_bytes(wire[24..32].try_into().expect("fixed slice length")),172 capture_timestamp_us: u64::from_be_bytes(173 wire[32..40].try_into().expect("fixed slice length"),174 ),175 pts_us: i64::from_be_bytes(wire[40..48].try_into().expect("fixed slice length")),176 };177 header.validate()?;178 Ok(header)179 }180}181182#[derive(Debug, Clone, PartialEq, Eq)]183pub struct AudioPacket {184 pub header: AudioPacketHeader,185 pub payload: Vec<u8>,186}187188impl AudioPacket {189 pub fn new(header: AudioPacketHeader, payload: Vec<u8>) -> Result<Self> {190 header.validate()?;191 if payload.len() != header.payload_len as usize {192 return Err(ProtocolError::InvalidMessage {193 message_type: "audio packet",194 reason: "payload does not match declared length",195 });196 }197 Ok(Self { header, payload })198 }199200 pub fn encode_datagram(&self) -> Result<Vec<u8>> {201 self.header.validate()?;202 if self.payload.len() != self.header.payload_len as usize {203 return Err(ProtocolError::InvalidMessage {204 message_type: "audio packet",205 reason: "payload does not match declared length",206 });207 }208 let mut datagram = Vec::with_capacity(AUDIO_HEADER_LEN + self.payload.len());209 datagram.extend_from_slice(&self.header.encode()?);210 datagram.extend_from_slice(&self.payload);211 Ok(datagram)212 }213214 pub fn decode_datagram(datagram: &[u8]) -> Result<Self> {215 if datagram.len() < AUDIO_HEADER_LEN {216 return Err(ProtocolError::InvalidMessage {217 message_type: "audio packet",218 reason: "datagram is shorter than the audio header",219 });220 }221 let wire: &[u8; AUDIO_HEADER_LEN] = datagram[..AUDIO_HEADER_LEN]222 .try_into()223 .expect("checked fixed slice length");224 let header = AudioPacketHeader::decode(wire)?;225 if datagram.len() != AUDIO_HEADER_LEN + header.payload_len as usize {226 return Err(ProtocolError::InvalidMessage {227 message_type: "audio packet",228 reason: "datagram does not match declared payload length",229 });230 }231 Self::new(header, datagram[AUDIO_HEADER_LEN..].to_vec())232 }233}234235#[cfg(test)]236mod tests {237 use super::*;238239 fn packet() -> AudioPacket {240 let payload = vec![0xf8, 0xff, 0xfe];241 AudioPacket::new(242 AudioPacketHeader::opus(243 payload.len(),244 42,245 123_456,246 120_000,247 AudioFlags::DISCONTINUITY,248 )249 .unwrap(),250 payload,251 )252 .unwrap()253 }254255 #[test]256 fn audio_datagram_is_a_golden_vector() {257 let datagram = packet().encode_datagram().unwrap();258 assert_eq!(datagram.len(), AUDIO_HEADER_LEN + 3);259 assert_eq!(&datagram[0..4], b"SGA1");260 assert_eq!(&datagram[4..6], &PROTOCOL_VERSION.to_be_bytes());261 assert_eq!(datagram[6], 48);262 assert_eq!(datagram[7], 1);263 assert_eq!(datagram[8], AudioFlags::DISCONTINUITY.bits());264 assert_eq!(datagram[9], 2);265 assert_eq!(&datagram[12..16], &48_000_u32.to_be_bytes());266 assert_eq!(&datagram[16..18], &960_u16.to_be_bytes());267 assert_eq!(&datagram[20..24], &3_u32.to_be_bytes());268 assert_eq!(&datagram[24..32], &42_u64.to_be_bytes());269 assert_eq!(&datagram[32..40], &123_456_u64.to_be_bytes());270 assert_eq!(&datagram[40..48], &120_000_i64.to_be_bytes());271 assert_eq!(&datagram[48..], &[0xf8, 0xff, 0xfe]);272 assert_eq!(AudioPacket::decode_datagram(&datagram).unwrap(), packet());273 }274275 #[test]276 fn rejects_malformed_audio_datagrams_before_payload_use() {277 let valid = packet().encode_datagram().unwrap();278 for index in [0, 4, 6, 7, 8, 9, 10, 12, 16, 18, 20] {279 let mut corrupt = valid.clone();280 corrupt[index] ^= 0xff;281 assert!(282 AudioPacket::decode_datagram(&corrupt).is_err(),283 "accepted corruption at byte {index}"284 );285 }286 assert!(AudioPacket::decode_datagram(&valid[..AUDIO_HEADER_LEN - 1]).is_err());287 assert!(AudioPacket::decode_datagram(&valid[..valid.len() - 1]).is_err());288 let mut trailing = valid;289 trailing.push(0);290 assert!(AudioPacket::decode_datagram(&trailing).is_err());291 }292293 #[test]294 fn audio_payload_and_format_are_strictly_bounded() {295 assert!(AudioPacketHeader::opus(0, 0, 0, 0, AudioFlags::NONE).is_err());296 assert!(297 AudioPacketHeader::opus(MAX_AUDIO_PAYLOAD_LEN + 1, 0, 0, 0, AudioFlags::NONE).is_err()298 );299 let mut header = packet().header;300 header.channels = 1;301 assert!(header.validate().is_err());302 header = packet().header;303 header.sample_rate = 44_100;304 assert!(header.validate().is_err());305 header = packet().header;306 header.frame_samples = 480;307 assert!(header.validate().is_err());308 }309310 #[test]311 fn declared_and_actual_payload_must_match() {312 let header = AudioPacketHeader::opus(2, 0, 0, 0, AudioFlags::NONE).unwrap();313 assert!(AudioPacket::new(header, vec![1]).is_err());314 }315}