Skip to main content

oximedia_codec/
silk.rs

1//! Standalone SILK frame decoding types and scaffolding.
2//!
3//! SILK (Skype Low Latency Audio Codec) is the speech codec used within Opus.
4//! This module provides lightweight frame-level types for parsing SILK frame
5//! headers and applying LPC synthesis, independent of the full Opus decoder
6//! pipeline.
7
8/// SILK operating bandwidth.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum SilkBandwidth {
11    /// Narrowband — 8 kHz sample rate.
12    NarrowBand,
13    /// Medium band — 12 kHz sample rate.
14    MediumBand,
15    /// Wideband — 16 kHz sample rate.
16    WideBand,
17    /// Super-wideband — 24 kHz sample rate.
18    SuperWideBand,
19}
20
21impl SilkBandwidth {
22    /// Returns the sample rate in Hz associated with this bandwidth.
23    pub fn sample_rate(&self) -> u32 {
24        match self {
25            Self::NarrowBand => 8_000,
26            Self::MediumBand => 12_000,
27            Self::WideBand => 16_000,
28            Self::SuperWideBand => 24_000,
29        }
30    }
31}
32
33/// Parsed header fields from a SILK frame.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct SilkFrameHeader {
36    /// Voice activity detection flag.
37    pub vad_flag: bool,
38    /// Low Bitrate Redundancy (LBRR) payload present.
39    pub lbrr_flag: bool,
40    /// Signal type: 0 = inactive, 1 = voiced, 2 = unvoiced.
41    pub signal_type: u8,
42    /// Quantization offset type (0 or 1).
43    pub quantization_offset: u8,
44}
45
46impl SilkFrameHeader {
47    /// Parses a `SilkFrameHeader` from the first byte(s) of a raw frame.
48    ///
49    /// The SILK frame header is packed into the leading bits of the payload.
50    /// This parser reads the minimum information needed to scaffold frame
51    /// processing; full SILK decoding requires an entropy/range decoder.
52    ///
53    /// Layout of byte 0:
54    /// ```text
55    /// Bit 7: VAD flag
56    /// Bit 6: LBRR flag
57    /// Bits 5-4: signal_type (0-2, values 0b11 treated as inactive)
58    /// Bit 3: quantization_offset
59    /// Bits 2-0: reserved / additional payload bits
60    /// ```
61    pub fn parse(data: &[u8]) -> Result<Self, String> {
62        if data.is_empty() {
63            return Err("SILK frame data is empty".to_string());
64        }
65
66        let b0 = data[0];
67
68        let vad_flag = (b0 & 0x80) != 0;
69        let lbrr_flag = (b0 & 0x40) != 0;
70        let raw_signal = (b0 >> 4) & 0x03;
71        let signal_type = if raw_signal > 2 { 0 } else { raw_signal };
72        let quantization_offset = (b0 >> 3) & 0x01;
73
74        Ok(Self {
75            vad_flag,
76            lbrr_flag,
77            signal_type,
78            quantization_offset,
79        })
80    }
81}
82
83/// LPC (Linear Predictive Coding) filter coefficients for one SILK subframe.
84///
85/// Coefficients are stored in Q12 fixed-point format (i.e. the value `4096`
86/// represents 1.0).
87#[derive(Debug, Clone)]
88pub struct SilkLpcCoeffs {
89    /// LPC filter order (10 for narrowband/mediumband, 16 for wideband/super-wideband).
90    pub order: usize,
91    /// LPC filter coefficients in Q12 fixed-point.
92    pub coeffs: Vec<i16>,
93}
94
95impl SilkLpcCoeffs {
96    /// Creates a zeroed `SilkLpcCoeffs` with the given filter order.
97    pub fn new(order: usize) -> Self {
98        Self {
99            order,
100            coeffs: vec![0i16; order],
101        }
102    }
103}
104
105/// A decoded SILK frame.
106#[derive(Debug, Clone)]
107pub struct SilkFrame {
108    /// Parsed frame header.
109    pub header: SilkFrameHeader,
110    /// LPC coefficients used for synthesis.
111    pub lpc: SilkLpcCoeffs,
112    /// Decoded PCM samples (i16, linear).
113    pub samples: Vec<i16>,
114    /// Number of samples in this frame.
115    pub sample_count: usize,
116}
117
118impl SilkFrame {
119    /// Creates an empty `SilkFrame` with default-zeroed fields.
120    pub fn new() -> Self {
121        Self {
122            header: SilkFrameHeader {
123                vad_flag: false,
124                lbrr_flag: false,
125                signal_type: 0,
126                quantization_offset: 0,
127            },
128            lpc: SilkLpcCoeffs::new(16),
129            samples: Vec::new(),
130            sample_count: 0,
131        }
132    }
133
134    /// Returns the number of PCM samples in this frame.
135    pub fn sample_count(&self) -> usize {
136        self.sample_count
137    }
138
139    /// Returns the samples normalised to the range `[-1.0, 1.0]` as `f32`.
140    pub fn as_f32_samples(&self) -> Vec<f32> {
141        self.samples
142            .iter()
143            .map(|&s| s as f32 / i16::MAX as f32)
144            .collect()
145    }
146}
147
148/// SILK frame decoder scaffold.
149///
150/// This type parses the SILK frame header and exposes helpers for LPC
151/// synthesis. Full entropy-coded SILK decoding is extremely complex and is
152/// provided by the Opus implementation in `crate::opus::silk`. This struct
153/// is intentionally lightweight and suitable for testing and scaffolding.
154#[derive(Debug)]
155pub struct SilkDecoder {
156    /// Operating bandwidth.
157    pub bandwidth: SilkBandwidth,
158    /// Expected frame size in samples.
159    pub frame_size: usize,
160    /// Previous output samples kept for LPC synthesis state (history).
161    pub prev_samples: Vec<i16>,
162}
163
164impl SilkDecoder {
165    /// Creates a new `SilkDecoder` for the given bandwidth.
166    pub fn new(bandwidth: SilkBandwidth) -> Self {
167        // SILK uses 20 ms frames.
168        let frame_size = (bandwidth.sample_rate() as usize) * 20 / 1000;
169        // LPC order is 16 for WB/SWB, 10 for NB/MB.
170        let lpc_order = match bandwidth {
171            SilkBandwidth::NarrowBand | SilkBandwidth::MediumBand => 10,
172            SilkBandwidth::WideBand | SilkBandwidth::SuperWideBand => 16,
173        };
174        Self {
175            bandwidth,
176            frame_size,
177            prev_samples: vec![0i16; lpc_order],
178        }
179    }
180
181    /// Parses the frame header and returns a `SilkFrame` with zeroed samples.
182    ///
183    /// Full SILK decoding (excitation decoding, LTP, noise shaping …) is not
184    /// implemented here; the goal of this method is to validate the header and
185    /// set up the frame scaffold so that higher-level code can fill in the
186    /// decoded samples.
187    pub fn decode_frame(&mut self, data: &[u8]) -> Result<SilkFrame, String> {
188        let header = SilkFrameHeader::parse(data)?;
189
190        let lpc_order = self.prev_samples.len();
191        let lpc = SilkLpcCoeffs::new(lpc_order);
192
193        let samples = vec![0i16; self.frame_size];
194        let sample_count = self.frame_size;
195
196        Ok(SilkFrame {
197            header,
198            lpc,
199            samples,
200            sample_count,
201        })
202    }
203
204    /// Applies the LPC synthesis filter to an excitation signal.
205    ///
206    /// The synthesis filter is:
207    /// ```text
208    /// s[n] = excitation[n] + sum_{k=0}^{order-1} (lpc.coeffs[k] * s[n-k-1]) / 4096
209    /// ```
210    /// where the division by 4096 converts Q12 fixed-point coefficients back
211    /// to the integer domain.
212    ///
213    /// The decoder's `prev_samples` buffer is used as the initial state and
214    /// is updated to the last `order` samples of the output on return.
215    pub fn apply_lpc_synthesis(&mut self, excitation: &[i16], lpc: &SilkLpcCoeffs) -> Vec<i16> {
216        let order = lpc.order;
217        let n = excitation.len();
218        let mut output = vec![0i16; n];
219
220        // Combined history: prev_samples (oldest…newest) ++ output produced so far.
221        // We index as: index_in_history(-1) = prev_samples[order - 1], etc.
222        let history: Vec<i16> = self.prev_samples.clone();
223
224        for i in 0..n {
225            let mut acc: i64 = excitation[i] as i64;
226            for k in 0..order {
227                // s[n - k - 1]: look back k+1 samples.
228                let back = k + 1;
229                let sample = if back <= i {
230                    output[i - back] as i64
231                } else {
232                    // Still in the prev_samples history.
233                    let hist_idx = order as isize - (back as isize - i as isize);
234                    if hist_idx >= 0 && (hist_idx as usize) < history.len() {
235                        history[hist_idx as usize] as i64
236                    } else {
237                        0i64
238                    }
239                };
240                acc += (lpc.coeffs[k] as i64 * sample) >> 12;
241            }
242            output[i] = acc.clamp(i16::MIN as i64, i16::MAX as i64) as i16;
243        }
244
245        // Update history with the last `order` output samples.
246        let keep = order.min(n);
247        let src_start = n - keep;
248        for (dst, src) in self.prev_samples[order - keep..]
249            .iter_mut()
250            .zip(output[src_start..].iter())
251        {
252            *dst = *src;
253        }
254
255        output
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use super::*;
262
263    #[test]
264    fn test_silk_bandwidth_sample_rate_narrowband() {
265        assert_eq!(SilkBandwidth::NarrowBand.sample_rate(), 8_000);
266    }
267
268    #[test]
269    fn test_silk_bandwidth_sample_rate_mediumband() {
270        assert_eq!(SilkBandwidth::MediumBand.sample_rate(), 12_000);
271    }
272
273    #[test]
274    fn test_silk_bandwidth_sample_rate_wideband() {
275        assert_eq!(SilkBandwidth::WideBand.sample_rate(), 16_000);
276    }
277
278    #[test]
279    fn test_silk_bandwidth_sample_rate_superwideband() {
280        assert_eq!(SilkBandwidth::SuperWideBand.sample_rate(), 24_000);
281    }
282
283    #[test]
284    fn test_silk_frame_header_parse_basic() {
285        // Byte 0: VAD=1, LBRR=0, signal=01 (voiced), q_offset=0
286        // 1 0 01 0 000 = 0b10010000 = 0x90
287        let data = [0x90u8];
288        let hdr = SilkFrameHeader::parse(&data).expect("should succeed");
289        assert!(hdr.vad_flag);
290        assert!(!hdr.lbrr_flag);
291        assert_eq!(hdr.signal_type, 1);
292        assert_eq!(hdr.quantization_offset, 0);
293    }
294
295    #[test]
296    fn test_silk_frame_header_parse_empty_returns_error() {
297        let result = SilkFrameHeader::parse(&[]);
298        assert!(result.is_err());
299    }
300
301    #[test]
302    fn test_silk_decoder_new() {
303        let dec = SilkDecoder::new(SilkBandwidth::WideBand);
304        assert_eq!(dec.bandwidth, SilkBandwidth::WideBand);
305        // 16000 Hz * 20ms = 320 samples
306        assert_eq!(dec.frame_size, 320);
307    }
308
309    #[test]
310    fn test_silk_decoder_decode_frame() {
311        let mut dec = SilkDecoder::new(SilkBandwidth::NarrowBand);
312        let data = [0x00u8; 10];
313        let frame = dec.decode_frame(&data).expect("should succeed");
314        // 8000 Hz * 20ms = 160 samples
315        assert_eq!(frame.sample_count(), 160);
316        assert_eq!(frame.samples.len(), 160);
317    }
318
319    #[test]
320    fn test_silk_lpc_synthesis_zero_excitation_gives_zero_output() {
321        let mut dec = SilkDecoder::new(SilkBandwidth::NarrowBand);
322        let excitation = vec![0i16; 160];
323        let lpc = SilkLpcCoeffs::new(10);
324        let output = dec.apply_lpc_synthesis(&excitation, &lpc);
325        assert!(output.iter().all(|&s| s == 0));
326    }
327
328    #[test]
329    fn test_silk_frame_as_f32_samples_i16_max() {
330        let mut frame = SilkFrame::new();
331        frame.samples = vec![i16::MAX];
332        frame.sample_count = 1;
333        let f32s = frame.as_f32_samples();
334        assert!((f32s[0] - 1.0f32).abs() < 1e-4);
335    }
336
337    #[test]
338    fn test_silk_frame_as_f32_samples_i16_min() {
339        let mut frame = SilkFrame::new();
340        frame.samples = vec![i16::MIN];
341        frame.sample_count = 1;
342        let f32s = frame.as_f32_samples();
343        // i16::MIN as f32 / i16::MAX as f32 ≈ -1.00003…
344        assert!(f32s[0] < -0.999);
345    }
346
347    #[test]
348    fn test_silk_lpc_coeffs_new() {
349        let lpc = SilkLpcCoeffs::new(10);
350        assert_eq!(lpc.order, 10);
351        assert_eq!(lpc.coeffs.len(), 10);
352        assert!(lpc.coeffs.iter().all(|&c| c == 0));
353    }
354}