mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-28 12:36:56 -06:00
197 lines
7.8 KiB
Rust
197 lines
7.8 KiB
Rust
//! Opus identification header (`OpusHead`, RFC 7845 §5.1) and the ISO-BMFF `dOps` box payload.
|
|
|
|
use crate::{Error, Result};
|
|
|
|
/// Decoder configuration: channel layout, multistream mapping, pre-skip and output gain.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub struct OpusHead {
|
|
/// Output channel count (1..=255).
|
|
pub channels: usize,
|
|
/// Samples (at 48 kHz) to discard from the start of the decoded stream.
|
|
pub pre_skip: u16,
|
|
/// Sample rate of the original input (informational only).
|
|
pub input_sample_rate: u32,
|
|
/// Output gain in Q7.8 dB.
|
|
pub output_gain: i16,
|
|
/// Channel mapping family (0: mono/stereo, 1: Vorbis order surround, 255: unspecified order).
|
|
pub mapping_family: u8,
|
|
/// Number of elementary streams.
|
|
pub streams: usize,
|
|
/// Number of coupled (stereo) streams; these come first.
|
|
pub coupled_streams: usize,
|
|
/// Output channel → decoded channel index (255 = silence).
|
|
pub mapping: Vec<u8>,
|
|
}
|
|
|
|
impl OpusHead {
|
|
/// A family-0 configuration for plain mono/stereo streams (no header present).
|
|
pub fn simple(channels: usize) -> Result<OpusHead> {
|
|
if !(1..=2).contains(&channels) {
|
|
return Err(Error::InvalidArgument("family 0 supports 1 or 2 channels"));
|
|
}
|
|
Ok(OpusHead {
|
|
channels,
|
|
pre_skip: 0,
|
|
input_sample_rate: 48000,
|
|
output_gain: 0,
|
|
mapping_family: 0,
|
|
streams: 1,
|
|
coupled_streams: channels - 1,
|
|
mapping: (0..channels as u8).collect(),
|
|
})
|
|
}
|
|
|
|
/// Parses an `OpusHead` packet (Ogg first packet, Matroska `CodecPrivate`).
|
|
pub fn parse(data: &[u8]) -> Result<OpusHead> {
|
|
if data.len() < 19 || &data[..8] != b"OpusHead" {
|
|
return Err(Error::InvalidHeader("missing OpusHead magic"));
|
|
}
|
|
let version = data[8];
|
|
if version >> 4 != 0 {
|
|
return Err(Error::Unsupported("OpusHead major version"));
|
|
}
|
|
let channels = data[9] as usize;
|
|
let pre_skip = u16::from_le_bytes([data[10], data[11]]);
|
|
let input_sample_rate = u32::from_le_bytes([data[12], data[13], data[14], data[15]]);
|
|
let output_gain = i16::from_le_bytes([data[16], data[17]]);
|
|
let family = data[18];
|
|
Self::build(channels, pre_skip, input_sample_rate, output_gain, family, &data[19..])
|
|
}
|
|
|
|
/// Parses the payload of an ISO-BMFF `dOps` box (Opus in MP4, big-endian fields).
|
|
pub fn from_dops(data: &[u8]) -> Result<OpusHead> {
|
|
if data.len() < 11 {
|
|
return Err(Error::InvalidHeader("dOps too short"));
|
|
}
|
|
if data[0] != 0 {
|
|
return Err(Error::Unsupported("dOps version"));
|
|
}
|
|
let channels = data[1] as usize;
|
|
let pre_skip = u16::from_be_bytes([data[2], data[3]]);
|
|
let input_sample_rate = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
|
|
let output_gain = i16::from_be_bytes([data[8], data[9]]);
|
|
Self::build(channels, pre_skip, input_sample_rate, output_gain, data[10], &data[11..])
|
|
}
|
|
|
|
/// Serialises as an `OpusHead` packet.
|
|
pub fn to_bytes(&self) -> Vec<u8> {
|
|
let mut v = b"OpusHead".to_vec();
|
|
v.push(1);
|
|
v.push(self.channels as u8);
|
|
v.extend(self.pre_skip.to_le_bytes());
|
|
v.extend(self.input_sample_rate.to_le_bytes());
|
|
v.extend(self.output_gain.to_le_bytes());
|
|
v.push(self.mapping_family);
|
|
if self.mapping_family != 0 {
|
|
v.push(self.streams as u8);
|
|
v.push(self.coupled_streams as u8);
|
|
v.extend(&self.mapping);
|
|
}
|
|
v
|
|
}
|
|
|
|
fn build(channels: usize, pre_skip: u16, input_sample_rate: u32, output_gain: i16, family: u8, rest: &[u8]) -> Result<OpusHead> {
|
|
if channels == 0 {
|
|
return Err(Error::InvalidHeader("zero channels"));
|
|
}
|
|
let (streams, coupled, mapping) = if family == 0 {
|
|
if channels > 2 {
|
|
return Err(Error::InvalidHeader("family 0 allows at most 2 channels"));
|
|
}
|
|
(1, channels - 1, (0..channels as u8).collect::<Vec<_>>())
|
|
} else {
|
|
if family == 3 {
|
|
return Err(Error::Unsupported("channel mapping family 3 (projection)"));
|
|
}
|
|
if rest.len() < 2 + channels {
|
|
return Err(Error::InvalidHeader("truncated channel mapping table"));
|
|
}
|
|
let streams = rest[0] as usize;
|
|
let coupled = rest[1] as usize;
|
|
if streams == 0 || coupled > streams || streams + coupled > 255 {
|
|
return Err(Error::InvalidHeader("bad stream counts"));
|
|
}
|
|
if family == 1 && !(1..=8).contains(&channels) {
|
|
return Err(Error::InvalidHeader("family 1 allows 1 to 8 channels"));
|
|
}
|
|
let mapping = rest[2..2 + channels].to_vec();
|
|
if mapping.iter().any(|&m| m != 255 && m as usize >= streams + coupled) {
|
|
return Err(Error::InvalidHeader("channel mapping out of range"));
|
|
}
|
|
(streams, coupled, mapping)
|
|
};
|
|
Ok(OpusHead { channels, pre_skip, input_sample_rate, output_gain, mapping_family: family, streams, coupled_streams: coupled, mapping })
|
|
}
|
|
|
|
/// Checks the invariants the decoder indexes by, for headers built by hand rather than parsed.
|
|
pub fn validate(&self) -> Result<()> {
|
|
if self.channels == 0 {
|
|
return Err(Error::InvalidHeader("zero channels"));
|
|
}
|
|
if self.streams == 0 || self.coupled_streams > self.streams || self.streams + self.coupled_streams > 255 {
|
|
return Err(Error::InvalidHeader("bad stream counts"));
|
|
}
|
|
if self.mapping.len() != self.channels {
|
|
return Err(Error::InvalidHeader("channel mapping length differs from channel count"));
|
|
}
|
|
if self.mapping.iter().any(|&m| m != 255 && m as usize >= self.streams + self.coupled_streams) {
|
|
return Err(Error::InvalidHeader("channel mapping out of range"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Linear output gain factor.
|
|
pub fn gain_linear(&self) -> f32 {
|
|
10f32.powf(self.output_gain as f32 / (20.0 * 256.0))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_family0_and_roundtrip() {
|
|
let mut h = b"OpusHead".to_vec();
|
|
h.extend([1, 2, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0x00, 0x01, 0]);
|
|
let head = OpusHead::parse(&h).unwrap();
|
|
assert_eq!(head.channels, 2);
|
|
assert_eq!(head.pre_skip, 312);
|
|
assert_eq!(head.input_sample_rate, 48000);
|
|
assert_eq!(head.output_gain, 256);
|
|
assert_eq!((head.streams, head.coupled_streams), (1, 1));
|
|
assert_eq!(head.to_bytes(), h);
|
|
assert!((head.gain_linear() - 1.122).abs() < 1e-3);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_family1_51() {
|
|
let mut h = b"OpusHead".to_vec();
|
|
h.extend([1, 6, 0x38, 0x01, 0x80, 0xBB, 0, 0, 0, 0, 1, 4, 2, 0, 4, 1, 2, 3, 5]);
|
|
let head = OpusHead::parse(&h).unwrap();
|
|
assert_eq!((head.channels, head.streams, head.coupled_streams), (6, 4, 2));
|
|
assert_eq!(head.mapping, vec![0, 4, 1, 2, 3, 5]);
|
|
assert_eq!(OpusHead::parse(&head.to_bytes()).unwrap(), head);
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_garbage() {
|
|
assert!(OpusHead::parse(b"OpusHea").is_err());
|
|
assert!(OpusHead::parse(b"OpusTags...........").is_err());
|
|
let mut h = b"OpusHead".to_vec();
|
|
h.extend([1, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
|
|
assert!(OpusHead::parse(&h).is_err());
|
|
let mut h = b"OpusHead".to_vec();
|
|
h.extend([1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 5]);
|
|
assert!(OpusHead::parse(&h).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn dops() {
|
|
let d = [0u8, 2, 0x01, 0x38, 0, 0, 0xBB, 0x80, 0, 0, 0];
|
|
let head = OpusHead::from_dops(&d).unwrap();
|
|
assert_eq!(head.pre_skip, 312);
|
|
assert_eq!(head.channels, 2);
|
|
}
|
|
}
|