oxideav_opus/opus_head.rs
1//! `OpusHead` identification-header parsing — RFC 7845 §5.1 + §5.1.1.
2//!
3//! Every Ogg-Opus logical stream begins with an *identification header*
4//! (the "OpusHead" packet, RFC 7845 §5.1, Figure 2). It is the Opus
5//! decoder's *configuration*: how many output channels to produce, how
6//! many samples to discard at start-up (pre-skip), the original input
7//! sample rate (metadata only), an output gain, and — crucially for
8//! multichannel content — the *channel mapping* that ties one or more
9//! coded Opus streams to the stream's output channels (RFC 7845 §5.1.1,
10//! Figure 3).
11//!
12//! ## Why this lives in the codec crate
13//!
14//! `OpusHead` is the codec's own configuration record, not container
15//! framing. The Ogg layer (`oxideav-ogg`) merely delivers the raw header
16//! *packet bytes*; interpreting those bytes — validating the magic,
17//! version, and the §5.1.1 stream/coupled counts, and deciding how the
18//! per-stream decoder outputs combine into the final channels — is Opus
19//! decoding. The same header is also what a non-Ogg transport (e.g. a
20//! Matroska `CodecPrivate`) carries verbatim, so the parser belongs with
21//! the codec, like the §3 framing rules already do.
22//!
23//! ## What this module produces
24//!
25//! [`OpusHead::parse`] consumes a header packet and returns a fully
26//! validated [`OpusHead`] carrying the §5.1 scalar fields plus the
27//! §5.1.1 [`ChannelMappingTable`]. For mapping family 0 the table is
28//! synthesized from the defaults the RFC pins (N = 1, M = C − 1, the
29//! identity channel map) since family 0 omits the on-wire table.
30//!
31//! ## Provenance
32//!
33//! RFC 7845 §5.1 (Figure 2, pp. 12–15) for the scalar fields and §5.1.1
34//! (Figure 3, pp. 16–18) for the channel-mapping table, including the
35//! family-0 / family-1 / family-255 allowed-channel rules and every MUST
36//! validation (`Version`, `Output Channel Count`, `Stream Count`,
37//! `Coupled Stream Count`, and the `M + N ≤ 255` decoded-channel bound).
38
39/// The fixed 8-octet magic signature `"OpusHead"` (RFC 7845 §5.1, item
40/// 1). A valid identification header begins with exactly these bytes.
41pub const OPUS_HEAD_MAGIC: &[u8; 8] = b"OpusHead";
42
43/// The minimum byte length of an identification header packet: the
44/// 8-octet magic, plus version / channel-count (2), pre-skip (2), input
45/// sample rate (4), output gain (2), and the mapping-family octet (1).
46/// A family-0 header is exactly this long; family ≥ 1 headers append the
47/// §5.1.1 channel-mapping table.
48pub const OPUS_HEAD_MIN_LEN: usize = 19;
49
50/// The maximum recognized major version. RFC 7845 §5.1 item 2: an
51/// implementation "SHOULD accept any stream with a version number of
52/// '15' or less, and SHOULD assume any stream with a version number
53/// '16' or greater is incompatible." The major version is the upper
54/// nibble, so versions `0x00..=0x0F` are accepted.
55pub const OPUS_HEAD_MAX_VERSION: u8 = 15;
56
57/// Errors that can arise parsing an `OpusHead` identification header.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum OpusHeadError {
60 /// The packet is shorter than the smallest valid header.
61 TooShort {
62 /// Length supplied.
63 got: usize,
64 /// Minimum required ([`OPUS_HEAD_MIN_LEN`], or more once the
65 /// mapping family demands a channel-mapping table).
66 need: usize,
67 },
68 /// The leading 8 octets are not the [`OPUS_HEAD_MAGIC`] signature.
69 BadMagic,
70 /// The major version (upper nibble of the version octet) exceeds
71 /// [`OPUS_HEAD_MAX_VERSION`]; the stream is from an incompatible
72 /// future encapsulation revision (RFC 7845 §5.1 item 2).
73 IncompatibleVersion {
74 /// The raw version octet.
75 version: u8,
76 },
77 /// `Output Channel Count` is zero. RFC 7845 §5.1 item 3: "This value
78 /// MUST NOT be zero."
79 ZeroChannels,
80 /// The output channel count is out of range for the signalled
81 /// mapping family (family 0 allows 1..=2, family 1 allows 1..=8).
82 ChannelCountForFamily {
83 /// The mapping family octet.
84 family: u8,
85 /// The output channel count `C`.
86 channels: u8,
87 },
88 /// `Stream Count` N is zero. RFC 7845 §5.1.1 item 1: "This value
89 /// MUST NOT be zero."
90 ZeroStreams,
91 /// `Coupled Stream Count` M is larger than `Stream Count` N. RFC
92 /// 7845 §5.1.1 item 2: "This MUST be no larger than the total number
93 /// of streams, N."
94 CoupledExceedsStreams {
95 /// Stream count N.
96 streams: u8,
97 /// Coupled count M.
98 coupled: u8,
99 },
100 /// `M + N` (the total decoded channel count) exceeds 255. RFC 7845
101 /// §5.1.1 item 2: "The total number of decoded channels, (M + N),
102 /// MUST be no larger than 255."
103 TooManyDecodedChannels {
104 /// Stream count N.
105 streams: u8,
106 /// Coupled count M.
107 coupled: u8,
108 },
109 /// A per-output-channel mapping index is neither `< (M + N)` nor the
110 /// reserved silence value 255. RFC 7845 §5.1.1 item 3: each index
111 /// "MUST either be smaller than (M + N) or be the special value
112 /// 255."
113 MappingIndexOutOfRange {
114 /// The offending output-channel position.
115 output_channel: u8,
116 /// The index value found there.
117 index: u8,
118 /// The decoded-channel bound `M + N`.
119 decoded_channels: u8,
120 },
121}
122
123impl core::fmt::Display for OpusHeadError {
124 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
125 match self {
126 OpusHeadError::TooShort { got, need } => {
127 write!(f, "OpusHead too short: {got} bytes, need {need}")
128 }
129 OpusHeadError::BadMagic => write!(f, "OpusHead missing 'OpusHead' magic signature"),
130 OpusHeadError::IncompatibleVersion { version } => {
131 write!(
132 f,
133 "OpusHead major version {} > {OPUS_HEAD_MAX_VERSION}",
134 version >> 4
135 )
136 }
137 OpusHeadError::ZeroChannels => write!(f, "OpusHead output channel count is zero"),
138 OpusHeadError::ChannelCountForFamily { family, channels } => write!(
139 f,
140 "OpusHead channel count {channels} invalid for mapping family {family}"
141 ),
142 OpusHeadError::ZeroStreams => write!(f, "OpusHead stream count N is zero"),
143 OpusHeadError::CoupledExceedsStreams { streams, coupled } => {
144 write!(
145 f,
146 "OpusHead coupled count {coupled} exceeds stream count {streams}"
147 )
148 }
149 OpusHeadError::TooManyDecodedChannels { streams, coupled } => write!(
150 f,
151 "OpusHead decoded channels M+N = {}+{} exceeds 255",
152 coupled, streams
153 ),
154 OpusHeadError::MappingIndexOutOfRange {
155 output_channel,
156 index,
157 decoded_channels,
158 } => write!(
159 f,
160 "OpusHead mapping index {index} for output channel {output_channel} \
161 is neither < {decoded_channels} nor 255"
162 ),
163 }
164 }
165}
166
167impl std::error::Error for OpusHeadError {}
168
169/// The §5.1.1 channel-mapping table: how the `N` coded streams (the
170/// first `M` of which are stereo) combine into the stream's `C` output
171/// channels.
172///
173/// For mapping family 0 this is synthesized from the RFC-pinned defaults
174/// (the on-wire header omits the table entirely); for families ≥ 1 it is
175/// read from the header bytes following the mapping-family octet.
176#[derive(Debug, Clone, PartialEq, Eq)]
177pub struct ChannelMappingTable {
178 /// `N` — total number of coded Opus streams in each Ogg packet
179 /// (§5.1.1 item 1). Always ≥ 1.
180 pub stream_count: u8,
181 /// `M` — number of those streams decoded as stereo (§5.1.1 item 2).
182 /// The first `M` streams are stereo; the remaining `N − M` are mono.
183 pub coupled_count: u8,
184 /// One index per output channel (§5.1.1 item 3). `mapping[c]` selects
185 /// the decoded channel feeding output channel `c`:
186 ///
187 /// * `index < 2*M` → stereo stream `index/2`, left channel if even
188 /// else right.
189 /// * `2*M ≤ index < 255` → mono stream `index − M`.
190 /// * `index == 255` → pure silence.
191 pub mapping: Vec<u8>,
192}
193
194impl ChannelMappingTable {
195 /// Total decoded channel count `M + N` (RFC 7845 §5.1.1 item 2).
196 /// This is the number of distinct decoder channels the streams
197 /// produce before the [`Self::mapping`] selects output channels.
198 pub fn decoded_channels(&self) -> u16 {
199 self.coupled_count as u16 + self.stream_count as u16
200 }
201
202 /// The output channel count `C` — the length of [`Self::mapping`].
203 pub fn output_channels(&self) -> u8 {
204 self.mapping.len() as u8
205 }
206}
207
208/// A decoded `OpusHead` identification header (RFC 7845 §5.1).
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub struct OpusHead {
211 /// Raw version octet (RFC 7845 §5.1 item 2). The major version (upper
212 /// nibble) has been validated ≤ [`OPUS_HEAD_MAX_VERSION`].
213 pub version: u8,
214 /// Output channel count `C` (§5.1 item 3); always ≥ 1.
215 pub channel_count: u8,
216 /// Pre-skip: 48 kHz samples to discard at start-up (§5.1 item 4).
217 pub pre_skip: u16,
218 /// Original input sample rate in Hz; 0 means "unspecified" (§5.1
219 /// item 5). Metadata only — playback is always at 48 kHz here.
220 pub input_sample_rate: u32,
221 /// Output gain, Q7.8 dB, signed (§5.1 item 6). Apply via
222 /// `pow(10, gain / (20.0 * 256))`.
223 pub output_gain_q7_8: i16,
224 /// Channel mapping family octet (§5.1 item 7).
225 pub mapping_family: u8,
226 /// The resolved §5.1.1 channel-mapping table (synthesized for
227 /// family 0, parsed for families ≥ 1).
228 pub mapping: ChannelMappingTable,
229}
230
231impl OpusHead {
232 /// Parse and fully validate an identification-header packet per RFC
233 /// 7845 §5.1 + §5.1.1.
234 ///
235 /// Enforces every MUST in the two sections: the magic signature, the
236 /// version major-nibble bound, the non-zero channel count and its
237 /// per-family range, the non-zero stream count, `M ≤ N`,
238 /// `M + N ≤ 255`, and the per-output-channel mapping-index bound.
239 pub fn parse(packet: &[u8]) -> Result<Self, OpusHeadError> {
240 if packet.len() < OPUS_HEAD_MIN_LEN {
241 return Err(OpusHeadError::TooShort {
242 got: packet.len(),
243 need: OPUS_HEAD_MIN_LEN,
244 });
245 }
246 if &packet[0..8] != OPUS_HEAD_MAGIC.as_slice() {
247 return Err(OpusHeadError::BadMagic);
248 }
249 let version = packet[8];
250 if version >> 4 > 0 {
251 return Err(OpusHeadError::IncompatibleVersion { version });
252 }
253 let channel_count = packet[9];
254 if channel_count == 0 {
255 return Err(OpusHeadError::ZeroChannels);
256 }
257 let pre_skip = u16::from_le_bytes([packet[10], packet[11]]);
258 let input_sample_rate =
259 u32::from_le_bytes([packet[12], packet[13], packet[14], packet[15]]);
260 let output_gain_q7_8 = i16::from_le_bytes([packet[16], packet[17]]);
261 let mapping_family = packet[18];
262
263 let mapping = if mapping_family == 0 {
264 // §5.1.1.1 family 0: 1 or 2 channels; the table is omitted
265 // and synthesized from the pinned defaults (N = 1,
266 // M = C − 1, identity map).
267 if channel_count > 2 {
268 return Err(OpusHeadError::ChannelCountForFamily {
269 family: mapping_family,
270 channels: channel_count,
271 });
272 }
273 ChannelMappingTable {
274 stream_count: 1,
275 coupled_count: channel_count - 1,
276 mapping: (0..channel_count).collect(),
277 }
278 } else {
279 // §5.1.1 families ≥ 1 carry an explicit table: N (1 byte),
280 // M (1 byte), then C mapping octets.
281 // `channel_count` is already validated non-zero above, so
282 // the family-1 range check is just the upper bound of 8.
283 if mapping_family == 1 && channel_count > 8 {
284 return Err(OpusHeadError::ChannelCountForFamily {
285 family: mapping_family,
286 channels: channel_count,
287 });
288 }
289 let table_len = 2 + channel_count as usize;
290 let need = OPUS_HEAD_MIN_LEN + table_len;
291 if packet.len() < need {
292 return Err(OpusHeadError::TooShort {
293 got: packet.len(),
294 need,
295 });
296 }
297 let stream_count = packet[19];
298 let coupled_count = packet[20];
299 if stream_count == 0 {
300 return Err(OpusHeadError::ZeroStreams);
301 }
302 if coupled_count > stream_count {
303 return Err(OpusHeadError::CoupledExceedsStreams {
304 streams: stream_count,
305 coupled: coupled_count,
306 });
307 }
308 // M + N ≤ 255 (the index space cannot address more).
309 if coupled_count as u16 + stream_count as u16 > 255 {
310 return Err(OpusHeadError::TooManyDecodedChannels {
311 streams: stream_count,
312 coupled: coupled_count,
313 });
314 }
315 let decoded_channels = coupled_count + stream_count; // ≤ 255, fits u8.
316 let map_start = 21;
317 let mut mapping = Vec::with_capacity(channel_count as usize);
318 for c in 0..channel_count as usize {
319 let index = packet[map_start + c];
320 if index != 255 && index >= decoded_channels {
321 return Err(OpusHeadError::MappingIndexOutOfRange {
322 output_channel: c as u8,
323 index,
324 decoded_channels,
325 });
326 }
327 mapping.push(index);
328 }
329 ChannelMappingTable {
330 stream_count,
331 coupled_count,
332 mapping,
333 }
334 };
335
336 Ok(OpusHead {
337 version,
338 channel_count,
339 pre_skip,
340 input_sample_rate,
341 output_gain_q7_8,
342 mapping_family,
343 mapping,
344 })
345 }
346
347 /// Compose the identification-header packet for this `OpusHead` —
348 /// the write-side mirror of [`Self::parse`] (RFC 7845 §5.1 +
349 /// §5.1.1).
350 ///
351 /// Validates the same MUSTs the parser enforces before emitting a
352 /// single byte: the version major-nibble bound, the non-zero
353 /// channel count and its per-family range, a mapping-table length
354 /// equal to the channel count, the non-zero stream count, `M ≤ N`,
355 /// `M + N ≤ 255`, and the per-output-channel mapping-index bound.
356 /// For **family 0** the on-wire header omits the table (§5.1.1.1),
357 /// so the held table must equal the RFC-pinned synthesized default
358 /// (`N = 1`, `M = C − 1`, identity map) or composition fails —
359 /// otherwise the parse of the produced bytes would not reconstruct
360 /// this value. Successful output always reparses equal.
361 pub fn compose(&self) -> Result<Vec<u8>, OpusHeadError> {
362 if self.version >> 4 > 0 {
363 return Err(OpusHeadError::IncompatibleVersion {
364 version: self.version,
365 });
366 }
367 if self.channel_count == 0 {
368 return Err(OpusHeadError::ZeroChannels);
369 }
370 let mut out = Vec::with_capacity(OPUS_HEAD_MIN_LEN + 2 + self.channel_count as usize);
371 out.extend_from_slice(OPUS_HEAD_MAGIC);
372 out.push(self.version);
373 out.push(self.channel_count);
374 out.extend_from_slice(&self.pre_skip.to_le_bytes());
375 out.extend_from_slice(&self.input_sample_rate.to_le_bytes());
376 out.extend_from_slice(&self.output_gain_q7_8.to_le_bytes());
377 out.push(self.mapping_family);
378
379 if self.mapping_family == 0 {
380 // §5.1.1.1: C ≤ 2, table omitted; the held table must be
381 // exactly the synthesized default.
382 if self.channel_count > 2 {
383 return Err(OpusHeadError::ChannelCountForFamily {
384 family: 0,
385 channels: self.channel_count,
386 });
387 }
388 let default = ChannelMappingTable {
389 stream_count: 1,
390 coupled_count: self.channel_count - 1,
391 mapping: (0..self.channel_count).collect(),
392 };
393 if self.mapping != default {
394 // A family-0 header cannot carry a non-default table;
395 // surface it as the family/channel mismatch it is.
396 return Err(OpusHeadError::ChannelCountForFamily {
397 family: 0,
398 channels: self.channel_count,
399 });
400 }
401 } else {
402 if self.mapping_family == 1 && self.channel_count > 8 {
403 return Err(OpusHeadError::ChannelCountForFamily {
404 family: self.mapping_family,
405 channels: self.channel_count,
406 });
407 }
408 if self.mapping.mapping.len() != self.channel_count as usize {
409 return Err(OpusHeadError::TooShort {
410 got: OPUS_HEAD_MIN_LEN + 2 + self.mapping.mapping.len(),
411 need: OPUS_HEAD_MIN_LEN + 2 + self.channel_count as usize,
412 });
413 }
414 if self.mapping.stream_count == 0 {
415 return Err(OpusHeadError::ZeroStreams);
416 }
417 if self.mapping.coupled_count > self.mapping.stream_count {
418 return Err(OpusHeadError::CoupledExceedsStreams {
419 streams: self.mapping.stream_count,
420 coupled: self.mapping.coupled_count,
421 });
422 }
423 if self.mapping.decoded_channels() > 255 {
424 return Err(OpusHeadError::TooManyDecodedChannels {
425 streams: self.mapping.stream_count,
426 coupled: self.mapping.coupled_count,
427 });
428 }
429 // `M + N ≤ 255` was checked above, so the u8 sum cannot wrap.
430 let decoded_channels = self.mapping.coupled_count + self.mapping.stream_count;
431 out.push(self.mapping.stream_count);
432 out.push(self.mapping.coupled_count);
433 for (c, &index) in self.mapping.mapping.iter().enumerate() {
434 if index != 255 && index >= decoded_channels {
435 return Err(OpusHeadError::MappingIndexOutOfRange {
436 output_channel: c as u8,
437 index,
438 decoded_channels,
439 });
440 }
441 out.push(index);
442 }
443 }
444 Ok(out)
445 }
446
447 /// Linear playback scale factor derived from the §5.1 output gain:
448 /// `pow(10, output_gain / (20.0 * 256))`. A gain of 0 returns 1.0.
449 pub fn output_gain_linear(&self) -> f64 {
450 10f64.powf(self.output_gain_q7_8 as f64 / (20.0 * 256.0))
451 }
452
453 /// Apply this header's §5.1 output gain in place to a buffer of
454 /// interleaved 48 kHz PCM. A zero gain is a no-op; otherwise every
455 /// sample is scaled by [`Self::output_gain_linear`] and saturated to
456 /// the `i16` range. See [`apply_output_gain`].
457 pub fn apply_gain(&self, pcm: &mut [i16]) {
458 apply_output_gain(pcm, self.output_gain_q7_8);
459 }
460}
461
462/// Apply a §5.1 output gain (raw Q7.8 dB value) in place to a buffer of
463/// PCM samples, saturating to the `i16` range.
464///
465/// RFC 7845 §5.1 item 6 defines the gain as a Q7.8 fixed-point dB value
466/// and gives the application formula
467/// `sample *= pow(10, output_gain / (20.0 * 256))`. "Players and media
468/// frameworks SHOULD apply it by default." A gain of 0 leaves the buffer
469/// untouched (the common case — muxers SHOULD write zero and bake any
470/// gain into the encode).
471pub fn apply_output_gain(pcm: &mut [i16], gain_q7_8: i16) {
472 if gain_q7_8 == 0 {
473 return;
474 }
475 let scale = 10f64.powf(gain_q7_8 as f64 / (20.0 * 256.0));
476 for s in pcm.iter_mut() {
477 let scaled = (*s as f64 * scale).round();
478 *s = scaled.clamp(i16::MIN as f64, i16::MAX as f64) as i16;
479 }
480}
481
482/// Pre-skip accumulator (RFC 7845 §5.1 item 4 / §4.2 trimming).
483///
484/// The §5.1 pre-skip is the number of 48 kHz samples (per channel) to
485/// discard from the *start* of the decoded output, giving the decoder's
486/// internal filters time to converge before audible playback begins.
487/// This helper threads the remaining pre-skip count across packets: feed
488/// it each decoded packet's per-channel sample count and it reports how
489/// many leading per-channel samples of that packet to drop.
490///
491/// Trimming is applied to the per-channel sample stream; for interleaved
492/// PCM with `C` channels, multiply the returned count by `C` to get the
493/// number of interleaved samples to drop from the front of the buffer.
494#[derive(Debug, Clone, Copy, PartialEq, Eq)]
495pub struct PreSkip {
496 remaining: u32,
497}
498
499impl PreSkip {
500 /// Construct a pre-skip accumulator with `pre_skip` per-channel
501 /// samples still to discard.
502 pub fn new(pre_skip: u16) -> Self {
503 PreSkip {
504 remaining: pre_skip as u32,
505 }
506 }
507
508 /// Build the pre-skip accumulator for a parsed [`OpusHead`].
509 pub fn from_head(head: &OpusHead) -> Self {
510 Self::new(head.pre_skip)
511 }
512
513 /// Per-channel samples still to discard.
514 pub fn remaining(&self) -> u32 {
515 self.remaining
516 }
517
518 /// `true` once the pre-skip region has been fully consumed.
519 pub fn is_done(&self) -> bool {
520 self.remaining == 0
521 }
522
523 /// Register `samples_per_channel` newly-decoded per-channel samples
524 /// and return how many of them (from the front) fall inside the
525 /// pre-skip region and must be discarded. The remaining count is
526 /// advanced accordingly.
527 pub fn consume(&mut self, samples_per_channel: usize) -> usize {
528 let drop = (samples_per_channel as u32).min(self.remaining);
529 self.remaining -= drop;
530 drop as usize
531 }
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537
538 /// Build a minimal family-0 header with the given channel count.
539 fn family0_header(channels: u8) -> Vec<u8> {
540 let mut h = Vec::new();
541 h.extend_from_slice(OPUS_HEAD_MAGIC);
542 h.push(1); // version
543 h.push(channels);
544 h.extend_from_slice(&3840u16.to_le_bytes()); // pre-skip
545 h.extend_from_slice(&48000u32.to_le_bytes()); // input rate
546 h.extend_from_slice(&0i16.to_le_bytes()); // gain
547 h.push(0); // mapping family 0
548 h
549 }
550
551 #[test]
552 fn family0_mono_defaults() {
553 let head = OpusHead::parse(&family0_header(1)).unwrap();
554 assert_eq!(head.channel_count, 1);
555 assert_eq!(head.pre_skip, 3840);
556 assert_eq!(head.input_sample_rate, 48000);
557 assert_eq!(head.mapping.stream_count, 1);
558 assert_eq!(head.mapping.coupled_count, 0);
559 assert_eq!(head.mapping.mapping, vec![0]);
560 assert_eq!(head.mapping.decoded_channels(), 1);
561 }
562
563 #[test]
564 fn family0_stereo_defaults() {
565 let head = OpusHead::parse(&family0_header(2)).unwrap();
566 assert_eq!(head.mapping.stream_count, 1);
567 assert_eq!(head.mapping.coupled_count, 1); // C − 1
568 assert_eq!(head.mapping.mapping, vec![0, 1]);
569 assert_eq!(head.mapping.decoded_channels(), 2);
570 }
571
572 #[test]
573 fn family0_rejects_more_than_two_channels() {
574 assert_eq!(
575 OpusHead::parse(&family0_header(3)),
576 Err(OpusHeadError::ChannelCountForFamily {
577 family: 0,
578 channels: 3,
579 })
580 );
581 }
582
583 #[test]
584 fn bad_magic_rejected() {
585 let mut h = family0_header(1);
586 h[0] = b'X';
587 assert_eq!(OpusHead::parse(&h), Err(OpusHeadError::BadMagic));
588 }
589
590 #[test]
591 fn too_short_rejected() {
592 let h = vec![0u8; 10];
593 assert_eq!(
594 OpusHead::parse(&h),
595 Err(OpusHeadError::TooShort { got: 10, need: 19 })
596 );
597 }
598
599 #[test]
600 fn incompatible_major_version_rejected() {
601 let mut h = family0_header(1);
602 h[8] = 0x10; // major version 1 ⇒ incompatible
603 assert_eq!(
604 OpusHead::parse(&h),
605 Err(OpusHeadError::IncompatibleVersion { version: 0x10 })
606 );
607 // Minor-version bump within major 0 stays compatible.
608 let mut h = family0_header(1);
609 h[8] = 0x0F;
610 assert!(OpusHead::parse(&h).is_ok());
611 }
612
613 #[test]
614 fn zero_channels_rejected() {
615 let mut h = family0_header(1);
616 h[9] = 0;
617 assert_eq!(OpusHead::parse(&h), Err(OpusHeadError::ZeroChannels));
618 }
619
620 /// A 6-channel (5.1) family-1 header: N = 4 streams, M = 2 coupled,
621 /// Vorbis-order identity mapping `[0,1,2,3,4,5]`.
622 fn family1_5_1_header() -> Vec<u8> {
623 let mut h = Vec::new();
624 h.extend_from_slice(OPUS_HEAD_MAGIC);
625 h.push(1);
626 h.push(6); // 6 output channels
627 h.extend_from_slice(&312u16.to_le_bytes());
628 h.extend_from_slice(&0u32.to_le_bytes());
629 h.extend_from_slice(&0i16.to_le_bytes());
630 h.push(1); // mapping family 1
631 h.push(4); // N
632 h.push(2); // M
633 h.extend_from_slice(&[0, 1, 2, 3, 4, 5]); // C mapping octets
634 h
635 }
636
637 #[test]
638 fn family1_surround_table() {
639 let head = OpusHead::parse(&family1_5_1_header()).unwrap();
640 assert_eq!(head.mapping_family, 1);
641 assert_eq!(head.channel_count, 6);
642 assert_eq!(head.mapping.stream_count, 4);
643 assert_eq!(head.mapping.coupled_count, 2);
644 assert_eq!(head.mapping.decoded_channels(), 6); // M + N
645 assert_eq!(head.mapping.mapping, vec![0, 1, 2, 3, 4, 5]);
646 assert_eq!(head.mapping.output_channels(), 6);
647 }
648
649 #[test]
650 fn family1_rejects_coupled_gt_streams() {
651 let mut h = family1_5_1_header();
652 h[20] = 5; // M = 5 > N = 4
653 assert_eq!(
654 OpusHead::parse(&h),
655 Err(OpusHeadError::CoupledExceedsStreams {
656 streams: 4,
657 coupled: 5,
658 })
659 );
660 }
661
662 #[test]
663 fn family1_rejects_zero_streams() {
664 let mut h = family1_5_1_header();
665 h[19] = 0;
666 assert_eq!(OpusHead::parse(&h), Err(OpusHeadError::ZeroStreams));
667 }
668
669 #[test]
670 fn family1_rejects_out_of_range_mapping_index() {
671 let mut h = family1_5_1_header();
672 h[21 + 5] = 6; // M + N = 6, so index 6 is out of range (and not 255)
673 assert_eq!(
674 OpusHead::parse(&h),
675 Err(OpusHeadError::MappingIndexOutOfRange {
676 output_channel: 5,
677 index: 6,
678 decoded_channels: 6,
679 })
680 );
681 // The reserved silence index 255 is always accepted.
682 let mut h = family1_5_1_header();
683 h[21 + 5] = 255;
684 assert!(OpusHead::parse(&h).is_ok());
685 }
686
687 #[test]
688 fn family1_rejects_truncated_table() {
689 let mut h = family1_5_1_header();
690 h.truncate(22); // cut into the mapping octets
691 assert!(matches!(
692 OpusHead::parse(&h),
693 Err(OpusHeadError::TooShort { .. })
694 ));
695 }
696
697 #[test]
698 fn family1_channel_count_bounds() {
699 // Family 1 allows 1..=8; 9 channels is rejected.
700 let mut h = Vec::new();
701 h.extend_from_slice(OPUS_HEAD_MAGIC);
702 h.push(1);
703 h.push(9);
704 h.extend_from_slice(&0u16.to_le_bytes());
705 h.extend_from_slice(&0u32.to_le_bytes());
706 h.extend_from_slice(&0i16.to_le_bytes());
707 h.push(1);
708 h.push(6);
709 h.push(2);
710 h.extend_from_slice(&[0, 1, 2, 3, 4, 5, 6, 7, 0]);
711 assert_eq!(
712 OpusHead::parse(&h),
713 Err(OpusHeadError::ChannelCountForFamily {
714 family: 1,
715 channels: 9,
716 })
717 );
718 }
719
720 #[test]
721 fn output_gain_linear_unity_at_zero() {
722 let head = OpusHead::parse(&family0_header(1)).unwrap();
723 assert!((head.output_gain_linear() - 1.0).abs() < 1e-12);
724 }
725
726 #[test]
727 fn output_gain_linear_positive_amplifies() {
728 let mut h = family0_header(1);
729 // +6.02 dB ≈ Q7.8 value 256*6 = 1536 → ~2x linear.
730 let g: i16 = 1536;
731 h[16..18].copy_from_slice(&g.to_le_bytes());
732 let head = OpusHead::parse(&h).unwrap();
733 assert!(head.output_gain_linear() > 1.9 && head.output_gain_linear() < 2.1);
734 }
735
736 #[test]
737 fn apply_output_gain_zero_is_noop() {
738 let mut pcm = vec![100i16, -200, 32767, -32768];
739 let before = pcm.clone();
740 apply_output_gain(&mut pcm, 0);
741 assert_eq!(pcm, before);
742 }
743
744 #[test]
745 fn apply_output_gain_doubles_at_plus_6db() {
746 // +6.02 dB ≈ Q7.8 1536 → ×2.
747 let mut pcm = vec![100i16, -100, 50];
748 apply_output_gain(&mut pcm, 1536);
749 assert_eq!(pcm[0], 200);
750 assert_eq!(pcm[1], -200);
751 assert_eq!(pcm[2], 100);
752 }
753
754 #[test]
755 fn apply_output_gain_saturates() {
756 // A large positive gain pushes a mid-scale sample past i16::MAX;
757 // it must clamp, not wrap.
758 let mut pcm = vec![20000i16, -20000];
759 apply_output_gain(&mut pcm, 1536); // ×2 → 40000 > 32767
760 assert_eq!(pcm[0], i16::MAX);
761 assert_eq!(pcm[1], i16::MIN);
762 }
763
764 #[test]
765 fn apply_gain_via_head() {
766 let mut h = family0_header(1);
767 h[16..18].copy_from_slice(&1536i16.to_le_bytes());
768 let head = OpusHead::parse(&h).unwrap();
769 let mut pcm = vec![10i16, -10];
770 head.apply_gain(&mut pcm);
771 assert_eq!(pcm, vec![20, -20]);
772 }
773
774 #[test]
775 fn pre_skip_consumes_across_packets() {
776 // 312-sample pre-skip (the fixture value) spread across 20 ms
777 // packets of 960 samples each: the first packet drops all 312,
778 // subsequent packets drop none.
779 let mut ps = PreSkip::new(312);
780 assert!(!ps.is_done());
781 assert_eq!(ps.remaining(), 312);
782 assert_eq!(ps.consume(960), 312);
783 assert!(ps.is_done());
784 assert_eq!(ps.consume(960), 0);
785 }
786
787 /// compose ∘ parse is the identity on header bytes: the family-0
788 /// mono/stereo defaults and the family-1 5.1 table all re-emit
789 /// byte-identically, and compose ∘ parse on a composed value
790 /// roundtrips the struct.
791 #[test]
792 fn compose_roundtrips_parse() {
793 for bytes in [family0_header(1), family0_header(2), family1_5_1_header()] {
794 let head = OpusHead::parse(&bytes).unwrap();
795 let composed = head.compose().unwrap();
796 assert_eq!(composed, bytes);
797 assert_eq!(OpusHead::parse(&composed).unwrap(), head);
798 }
799 }
800
801 /// compose enforces the same MUSTs as parse: bad version nibble,
802 /// zero channels, family-0 with a non-default table or > 2
803 /// channels, zero streams, M > N, oversized index space, mapping
804 /// length mismatch, and out-of-range mapping indices.
805 #[test]
806 fn compose_rejects_invalid_headers() {
807 let base = OpusHead::parse(&family1_5_1_header()).unwrap();
808
809 let mut h = base.clone();
810 h.version = 0x20;
811 assert!(matches!(
812 h.compose(),
813 Err(OpusHeadError::IncompatibleVersion { .. })
814 ));
815
816 let mut h = base.clone();
817 h.channel_count = 0;
818 assert_eq!(h.compose(), Err(OpusHeadError::ZeroChannels));
819
820 // Family 0 with a non-default table.
821 let mut h = OpusHead::parse(&family0_header(2)).unwrap();
822 h.mapping.mapping = vec![1, 0];
823 assert!(matches!(
824 h.compose(),
825 Err(OpusHeadError::ChannelCountForFamily { family: 0, .. })
826 ));
827
828 // Family 0 cannot carry more than 2 channels.
829 let mut h = OpusHead::parse(&family0_header(2)).unwrap();
830 h.channel_count = 3;
831 h.mapping.mapping = vec![0, 1, 2];
832 assert!(matches!(
833 h.compose(),
834 Err(OpusHeadError::ChannelCountForFamily { family: 0, .. })
835 ));
836
837 let mut h = base.clone();
838 h.mapping.stream_count = 0;
839 h.mapping.coupled_count = 0;
840 assert_eq!(h.compose(), Err(OpusHeadError::ZeroStreams));
841
842 let mut h = base.clone();
843 h.mapping.coupled_count = h.mapping.stream_count + 1;
844 assert!(matches!(
845 h.compose(),
846 Err(OpusHeadError::CoupledExceedsStreams { .. })
847 ));
848
849 let mut h = base.clone();
850 h.mapping.stream_count = 200;
851 h.mapping.coupled_count = 100;
852 assert!(matches!(
853 h.compose(),
854 Err(OpusHeadError::TooManyDecodedChannels { .. })
855 ));
856
857 // Mapping-table length must equal the channel count.
858 let mut h = base.clone();
859 h.mapping.mapping.pop();
860 assert!(matches!(h.compose(), Err(OpusHeadError::TooShort { .. })));
861
862 // Mapping index outside the decoded-channel space (and != 255).
863 let mut h = base.clone();
864 h.mapping.mapping[3] = 6; // decoded channels = M + N = 6 ⇒ max 5
865 assert!(matches!(
866 h.compose(),
867 Err(OpusHeadError::MappingIndexOutOfRange { .. })
868 ));
869 // ... while 255 (silence) is legal.
870 let mut h = base.clone();
871 h.mapping.mapping[3] = 255;
872 let bytes = h.compose().unwrap();
873 assert_eq!(OpusHead::parse(&bytes).unwrap(), h);
874 }
875
876 #[test]
877 fn pre_skip_spanning_multiple_packets() {
878 // A pre-skip larger than one packet drains over several.
879 let mut ps = PreSkip::new(1500);
880 assert_eq!(ps.consume(960), 960);
881 assert_eq!(ps.remaining(), 540);
882 assert_eq!(ps.consume(960), 540);
883 assert!(ps.is_done());
884 assert_eq!(ps.consume(960), 0);
885 }
886
887 #[test]
888 fn pre_skip_from_head() {
889 let head = OpusHead::parse(&family0_header(1)).unwrap();
890 // family0_header sets pre-skip 3840.
891 let ps = PreSkip::from_head(&head);
892 assert_eq!(ps.remaining(), 3840);
893 }
894}