Skip to main content

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}