Files
deckcraft/crates/opus/src/header.rs
T
Brandon ThomasandClaude Opus 5.5 4375fe7b89 Media playback: slidecraft-media (probe, audio decode, H.264/HEVC/VP9/AV1 video, mixer and clock), cpal output, editor control bar, slide show playback, media.* commands
- crates/media: probe (duration, rate/channels, size/fps; WMA/WMV recognised), audio decode to
  f32 PCM (MP3, AAC M4A/ADTS, ALAC, WAV, AIFF, CAF, FLAC, Vorbis via symphonia; Opus with our
  decoder in Ogg/WebM/MKV), video decode (MP4/MOV/WebM/MKV) with YUV->RGBA, poster frames,
  trim/fade/volume, Player mixer + per-voice clock (device-clocked or wall-clocked), VideoFeed.
- Codec crates ported from FilmCraft: bitstream, h264, hevc, vp9, av1, isobmff, matroska, ogg, opus.
- insert.audio/insert.video probe and store duration/size and a poster frame; unplayable media
  is embedded with a warning. New media.play/pause/toggle/stop/seek/info/posterFrame.
- UI: MediaHost (cpal output injected by the desktop app), control bar under media, video frames
  on canvas and in the show; autoplay, click to play, loop, rewind, across slides, hide, full screen.
- Tests with in-code WAV/AIFF and CC0 self-generated fixtures (make.sh, ATTRIBUTION.md rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 01:35:09 -04:00

180 lines
7.0 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 })
}
/// 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);
}
}