Skip to main content

oximedia_codec/vorbis/
codebook.rs

1//! Vorbis codebook (Huffman VQ book) abstraction.
2//!
3//! Vorbis uses custom Huffman books built from entries with associated vector
4//! dimensions. This module implements the essential codebook structure and
5//! lookup operations.
6
7#![forbid(unsafe_code)]
8#![allow(clippy::cast_possible_truncation)]
9
10/// A single codebook entry.
11#[derive(Clone, Debug)]
12pub struct CodebookEntry {
13    /// Huffman code length in bits (0 = unused entry).
14    pub length: u8,
15    /// The scalar or vector value(s) decoded when this entry is hit.
16    pub value: Vec<f32>,
17}
18
19/// A Vorbis codebook.
20#[derive(Clone, Debug)]
21pub struct Codebook {
22    /// Number of entries.
23    pub entries: usize,
24    /// Entry descriptors.
25    pub table: Vec<CodebookEntry>,
26    /// Vector dimension (1 for scalar books).
27    pub dimensions: usize,
28}
29
30impl Codebook {
31    /// Build a codebook from entry lengths and associated scalar values.
32    ///
33    /// `lengths[i]` is the Huffman code length for entry `i`.
34    /// `values[i]` is the decoded scalar value for entry `i` (dimension=1).
35    #[must_use]
36    pub fn from_lengths_and_values(lengths: &[u8], values: &[f32]) -> Self {
37        let entries = lengths.len();
38        let table = (0..entries)
39            .map(|i| CodebookEntry {
40                length: lengths[i],
41                value: vec![values[i.min(values.len() - 1)]],
42            })
43            .collect();
44        Self {
45            entries,
46            table,
47            dimensions: 1,
48        }
49    }
50
51    /// Look up the value for the entry with the given index.
52    ///
53    /// Returns `None` if the index is out of range or the entry is unused (length=0).
54    #[must_use]
55    pub fn lookup(&self, index: usize) -> Option<&[f32]> {
56        self.table
57            .get(index)
58            .filter(|e| e.length > 0)
59            .map(|e| e.value.as_slice())
60    }
61
62    /// Compute the average code length (entropy estimate).
63    #[must_use]
64    pub fn average_code_length(&self) -> f64 {
65        let active: Vec<u8> = self
66            .table
67            .iter()
68            .filter(|e| e.length > 0)
69            .map(|e| e.length)
70            .collect();
71        if active.is_empty() {
72            return 0.0;
73        }
74        active.iter().map(|&l| f64::from(l)).sum::<f64>() / active.len() as f64
75    }
76
77    /// Kraft inequality check: returns `true` if code lengths satisfy Kraft's inequality.
78    #[must_use]
79    pub fn kraft_inequality_satisfied(&self) -> bool {
80        let sum: f64 = self
81            .table
82            .iter()
83            .filter(|e| e.length > 0)
84            .map(|e| 2.0f64.powi(-(e.length as i32)))
85            .sum();
86        sum <= 1.0 + 1e-9
87    }
88}
89
90// =============================================================================
91// Tests
92// =============================================================================
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    fn simple_book() -> Codebook {
99        let lengths = [1u8, 2, 3, 3];
100        let values = [0.0f32, 1.0, 2.0, 3.0];
101        Codebook::from_lengths_and_values(&lengths, &values)
102    }
103
104    #[test]
105    fn test_codebook_entry_count() {
106        let book = simple_book();
107        assert_eq!(book.entries, 4);
108    }
109
110    #[test]
111    fn test_codebook_lookup_valid() {
112        let book = simple_book();
113        let v = book.lookup(0).expect("entry 0 should exist");
114        assert!((v[0] - 0.0).abs() < 1e-6);
115    }
116
117    #[test]
118    fn test_codebook_lookup_out_of_range() {
119        let book = simple_book();
120        assert!(book.lookup(10).is_none());
121    }
122
123    #[test]
124    fn test_codebook_average_length() {
125        let book = simple_book();
126        let avg = book.average_code_length();
127        // Lengths: 1, 2, 3, 3 → avg = (1+2+3+3)/4 = 2.25
128        assert!((avg - 2.25).abs() < 1e-9, "Expected 2.25, got {avg}");
129    }
130
131    #[test]
132    fn test_codebook_kraft_inequality() {
133        let book = simple_book();
134        // 2^-1 + 2^-2 + 2^-3 + 2^-3 = 0.5+0.25+0.125+0.125 = 1.0
135        assert!(book.kraft_inequality_satisfied());
136    }
137
138    #[test]
139    fn test_codebook_unused_entry_length_zero() {
140        let lengths = [0u8, 2, 0, 3];
141        let values = [0.0f32; 4];
142        let book = Codebook::from_lengths_and_values(&lengths, &values);
143        assert!(book.lookup(0).is_none()); // length=0 → unused
144        assert!(book.lookup(1).is_some());
145        assert!(book.lookup(2).is_none());
146    }
147
148    #[test]
149    fn test_codebook_dimension_is_one() {
150        let book = simple_book();
151        assert_eq!(book.dimensions, 1);
152    }
153}