Skip to main content

oximedia_codec/vorbis/
encoder.rs

1//! Vorbis audio encoder.
2//!
3//! This encoder implements the core Vorbis I encoding pipeline:
4//!
5//! 1. Input buffering (overlap-save framing)
6//! 2. MDCT analysis (windowed transform)
7//! 3. Floor curve fitting (spectral envelope)
8//! 4. Residue computation and VQ
9//! 5. Packet assembly (header + data)
10
11#![forbid(unsafe_code)]
12#![allow(clippy::cast_possible_truncation)]
13#![allow(clippy::cast_precision_loss)]
14#![allow(clippy::cast_sign_loss)]
15#![allow(clippy::cast_lossless)]
16
17use super::floor::{encode_floor1, Floor1Config, Floor1Curve};
18use super::mdct::MdctTwiddles;
19use super::residue::{ResidueConfig, ResidueEncoder, ResidueType};
20use crate::error::{CodecError, CodecResult};
21
22// =============================================================================
23// Quality / configuration
24// =============================================================================
25
26/// Vorbis quality preset (maps to approximate bitrate).
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum VorbisQuality {
29    /// Q0 ≈ 64 kbps (low quality).
30    Q0,
31    /// Q2 ≈ 96 kbps.
32    Q2,
33    /// Q5 ≈ 160 kbps (default).
34    Q5,
35    /// Q7 ≈ 224 kbps.
36    Q7,
37    /// Q10 ≈ 320 kbps (high quality).
38    Q10,
39}
40
41impl VorbisQuality {
42    /// Approximate target bitrate in bits-per-sample.
43    #[must_use]
44    pub fn bits_per_sample(&self) -> f64 {
45        match self {
46            Self::Q0 => 0.5,
47            Self::Q2 => 0.75,
48            Self::Q5 => 1.3,
49            Self::Q7 => 2.0,
50            Self::Q10 => 3.5,
51        }
52    }
53
54    /// Quantisation step size for residue VQ.
55    #[must_use]
56    pub fn residue_step(&self) -> f64 {
57        match self {
58            Self::Q0 => 0.5,
59            Self::Q2 => 0.35,
60            Self::Q5 => 0.2,
61            Self::Q7 => 0.12,
62            Self::Q10 => 0.06,
63        }
64    }
65}
66
67/// Vorbis encoder configuration.
68#[derive(Clone, Debug)]
69pub struct VorbisConfig {
70    /// Audio sample rate in Hz.
71    pub sample_rate: u32,
72    /// Number of audio channels.
73    pub channels: u8,
74    /// Quality preset.
75    pub quality: VorbisQuality,
76}
77
78// =============================================================================
79// Vorbis identification header
80// =============================================================================
81
82/// Generate the Vorbis identification header packet (first packet).
83fn make_id_header(sample_rate: u32, channels: u8, block0: u16, block1: u16) -> Vec<u8> {
84    let mut p = Vec::new();
85    p.push(1); // packet type = identification
86    p.extend_from_slice(b"vorbis");
87    p.extend_from_slice(&0u32.to_le_bytes()); // version
88    p.push(channels);
89    p.extend_from_slice(&sample_rate.to_le_bytes());
90    p.extend_from_slice(&0u32.to_le_bytes()); // bitrate_maximum
91    p.extend_from_slice(&0u32.to_le_bytes()); // bitrate_nominal
92    p.extend_from_slice(&0u32.to_le_bytes()); // bitrate_minimum
93                                              // blocksize_0 and blocksize_1 packed into one byte as log2 nibbles
94    let b0 = (block0 as f64).log2() as u8;
95    let b1 = (block1 as f64).log2() as u8;
96    p.push((b1 << 4) | (b0 & 0x0F));
97    p.push(1); // framing bit
98    p
99}
100
101/// Generate a stub comment header (second packet).
102fn make_comment_header() -> Vec<u8> {
103    let mut p = Vec::new();
104    p.push(3); // packet type = comment
105    p.extend_from_slice(b"vorbis");
106    let vendor = b"OxiMedia Vorbis Encoder";
107    p.extend_from_slice(&(vendor.len() as u32).to_le_bytes());
108    p.extend_from_slice(vendor);
109    p.extend_from_slice(&0u32.to_le_bytes()); // user comment count = 0
110    p.push(1); // framing
111    p
112}
113
114/// Generate a stub setup header (third packet) — empty books placeholder.
115fn make_setup_header() -> Vec<u8> {
116    let mut p = Vec::new();
117    p.push(5); // packet type = setup
118    p.extend_from_slice(b"vorbis");
119    // Minimal stub (real encoder would write codebooks/floor/residue/mapping/modes)
120    p.push(0); // codebook count - 1 = 0 (one empty book)
121    p.push(1); // framing
122    p
123}
124
125// =============================================================================
126// Encoder state
127// =============================================================================
128
129/// Vorbis audio encoder.
130pub struct VorbisEncoder {
131    config: VorbisConfig,
132    /// Short block MDCT (block size 256).
133    mdct_short: MdctTwiddles,
134    /// Long block MDCT (block size 2048).
135    mdct_long: MdctTwiddles,
136    /// Floor configuration.
137    floor_cfg: Floor1Config,
138    /// Residue encoder.
139    residue_enc: ResidueEncoder,
140    /// Sample input ring buffer (per channel).
141    buffer: Vec<Vec<f32>>,
142    /// Number of samples in the ring buffer.
143    buf_fill: usize,
144    /// Long block size.
145    block_size: usize,
146    /// Overlap size (half block).
147    overlap: usize,
148    /// Whether the header packets have been emitted.
149    headers_emitted: bool,
150}
151
152/// An encoded Vorbis packet.
153#[derive(Clone, Debug)]
154pub struct VorbisPacket {
155    /// Raw packet bytes (Ogg payload without page framing).
156    pub data: Vec<u8>,
157    /// Granule position (sample count up to end of packet).
158    pub granule_pos: u64,
159    /// Whether this is a header packet (not audio).
160    pub is_header: bool,
161}
162
163impl VorbisEncoder {
164    /// Create a new encoder from `config`.
165    ///
166    /// # Errors
167    ///
168    /// Returns `CodecError::InvalidParameter` if the configuration is invalid.
169    pub fn new(config: VorbisConfig) -> CodecResult<Self> {
170        if config.channels == 0 || config.channels > 8 {
171            return Err(CodecError::InvalidParameter(
172                "Vorbis supports 1-8 channels".to_string(),
173            ));
174        }
175        if config.sample_rate < 8000 || config.sample_rate > 192_000 {
176            return Err(CodecError::InvalidParameter(
177                "Sample rate must be 8000-192000 Hz".to_string(),
178            ));
179        }
180
181        let block_size = 2048usize;
182        let overlap = block_size / 2;
183
184        let floor_cfg = Floor1Config {
185            multiplier: 1,
186            partitions: 0,
187            partition_class_list: Vec::new(),
188            classes: Vec::new(),
189            x_list: (0..16u16).map(|i| i * (block_size as u16 / 32)).collect(),
190        };
191
192        let residue_cfg = ResidueConfig {
193            residue_type: ResidueType::Type1,
194            begin: 0,
195            end: (block_size / 2) as u32,
196            partition_size: 32,
197            classifications: 4,
198            classbook: 0,
199        };
200
201        let step = config.quality.residue_step();
202
203        Ok(Self {
204            mdct_short: MdctTwiddles::new(256),
205            mdct_long: MdctTwiddles::new(block_size),
206            floor_cfg,
207            residue_enc: ResidueEncoder::new(residue_cfg, step),
208            buffer: vec![vec![0.0f32; block_size * 2]; config.channels as usize],
209            buf_fill: 0,
210            block_size,
211            overlap,
212            headers_emitted: false,
213            config,
214        })
215    }
216
217    /// Return the three mandatory Vorbis header packets.
218    ///
219    /// Must be called before any audio encoding.
220    pub fn headers(&mut self) -> Vec<VorbisPacket> {
221        self.headers_emitted = true;
222        vec![
223            VorbisPacket {
224                data: make_id_header(
225                    self.config.sample_rate,
226                    self.config.channels,
227                    256,
228                    self.block_size as u16,
229                ),
230                granule_pos: 0,
231                is_header: true,
232            },
233            VorbisPacket {
234                data: make_comment_header(),
235                granule_pos: 0,
236                is_header: true,
237            },
238            VorbisPacket {
239                data: make_setup_header(),
240                granule_pos: 0,
241                is_header: true,
242            },
243        ]
244    }
245
246    /// Encode interleaved PCM samples (f32, range [-1, +1]).
247    ///
248    /// Returns zero or more audio packets.  Call `flush()` at end-of-stream.
249    ///
250    /// # Errors
251    ///
252    /// Returns `CodecError::InvalidParameter` if the sample count is not a
253    /// multiple of `channels`.
254    pub fn encode_interleaved(&mut self, samples: &[f32]) -> CodecResult<Vec<VorbisPacket>> {
255        let ch = self.config.channels as usize;
256        if samples.len() % ch != 0 {
257            return Err(CodecError::InvalidParameter(
258                "Sample count must be a multiple of channel count".to_string(),
259            ));
260        }
261
262        // Deinterleave into per-channel ring buffers
263        let frame_count = samples.len() / ch;
264        let mut out_packets = Vec::new();
265
266        for f in 0..frame_count {
267            for c in 0..ch {
268                let buf_pos = self.buf_fill + f;
269                if buf_pos < self.buffer[c].len() {
270                    self.buffer[c][buf_pos] = samples[f * ch + c];
271                }
272            }
273        }
274        self.buf_fill += frame_count;
275
276        // Emit packets whenever we have enough samples
277        while self.buf_fill >= self.block_size {
278            let pkt = self.encode_block()?;
279            out_packets.push(pkt);
280
281            // Shift buffer by `overlap` samples
282            for c in 0..ch {
283                self.buffer[c].copy_within(self.overlap..self.block_size, 0);
284                self.buf_fill -= self.overlap;
285            }
286        }
287
288        Ok(out_packets)
289    }
290
291    /// Flush any remaining buffered samples as a final packet.
292    pub fn flush(&mut self) -> CodecResult<Vec<VorbisPacket>> {
293        if self.buf_fill == 0 {
294            return Ok(Vec::new());
295        }
296        // Zero-pad to block_size
297        let ch = self.config.channels as usize;
298        for c in 0..ch {
299            for i in self.buf_fill..self.block_size {
300                self.buffer[c][i] = 0.0;
301            }
302        }
303        self.buf_fill = self.block_size;
304        let pkt = self.encode_block()?;
305        self.buf_fill = 0;
306        Ok(vec![pkt])
307    }
308
309    /// Encode one block from `self.buffer`, returning one audio packet.
310    fn encode_block(&self) -> CodecResult<VorbisPacket> {
311        let ch = self.config.channels as usize;
312        let n = self.block_size;
313        let mut pkt_data: Vec<u8> = Vec::new();
314        // Packet type byte (audio = 0)
315        pkt_data.push(0);
316
317        for c in 0..ch {
318            let mut windowed: Vec<f64> =
319                self.buffer[c][..n].iter().map(|&s| f64::from(s)).collect();
320            self.mdct_long.apply_window(&mut windowed);
321            let coeffs = self.mdct_long.forward(&windowed);
322
323            // Compute log-spectrum for floor fitting
324            let log_spec: Vec<f64> = coeffs
325                .iter()
326                .map(|&v| v.abs().max(1e-10_f64).log10())
327                .collect();
328
329            // Encode floor
330            let amps = encode_floor1(&log_spec, &self.floor_cfg.x_list, self.floor_cfg.multiplier);
331
332            // Compute residue (spectral coefficients - floor)
333            let floor_curve = Floor1Curve {
334                amplitudes: amps.clone(),
335                x_list: self.floor_cfg.x_list.clone(),
336                unused: false,
337            };
338            let floor_lin = floor_curve.to_linear(self.floor_cfg.multiplier);
339
340            let residue: Vec<f64> = coeffs
341                .iter()
342                .enumerate()
343                .map(|(i, &v)| {
344                    let fl = if i < floor_lin.len() {
345                        floor_lin[i]
346                    } else {
347                        1.0
348                    };
349                    v - fl
350                })
351                .collect();
352
353            // Quantise residue
354            let codes = self.residue_enc.quantise(&residue);
355
356            // Pack: floor amplitudes (i16 LE) + residue codes (i16 LE)
357            for a in &amps {
358                pkt_data.extend_from_slice(&a.to_le_bytes());
359            }
360            for &code in &codes {
361                pkt_data.extend_from_slice(&code.to_le_bytes());
362            }
363        }
364
365        Ok(VorbisPacket {
366            data: pkt_data,
367            granule_pos: 0, // would be set by muxer
368            is_header: false,
369        })
370    }
371}
372
373// =============================================================================
374// SimpleVorbisEncoder — thin API compatible with the requested interface
375// =============================================================================
376
377/// Simplified Vorbis encoder configuration.
378///
379/// `quality` ranges from −0.1 (very low) to 1.0 (maximum quality),
380/// matching the libvorbis VBR quality scale.
381#[derive(Clone, Debug)]
382pub struct VorbisEncConfig {
383    /// Audio sample rate in Hz (8000–192000).
384    pub sample_rate: u32,
385    /// Number of audio channels (1–8).
386    pub channels: u8,
387    /// Quality from −0.1 (lowest) to 1.0 (highest). Default: 0.5 ≈ Q5.
388    pub quality: f32,
389}
390
391impl Default for VorbisEncConfig {
392    fn default() -> Self {
393        Self {
394            sample_rate: 44100,
395            channels: 2,
396            quality: 0.5,
397        }
398    }
399}
400
401impl VorbisEncConfig {
402    /// Map the `[−0.1, 1.0]` quality range to a [`VorbisQuality`] preset.
403    fn to_quality_preset(&self) -> VorbisQuality {
404        let q = self.quality.clamp(-0.1, 1.0);
405        if q < 0.1 {
406            VorbisQuality::Q0
407        } else if q < 0.35 {
408            VorbisQuality::Q2
409        } else if q < 0.65 {
410            VorbisQuality::Q5
411        } else if q < 0.85 {
412            VorbisQuality::Q7
413        } else {
414            VorbisQuality::Q10
415        }
416    }
417}
418
419/// A simple Vorbis encoder that accepts interleaved f32 PCM and emits raw
420/// Ogg Vorbis payload bytes.
421///
422/// The first call to [`SimpleVorbisEncoder::encode_pcm`] prepends the three
423/// mandatory Vorbis header packets so the output is a self-contained stream.
424///
425/// # Example
426///
427/// ```ignore
428/// use oximedia_codec::vorbis::{SimpleVorbisEncoder, VorbisEncConfig};
429///
430/// let cfg = VorbisEncConfig {
431///     sample_rate: 44100,
432///     channels: 2,
433///     quality: 0.5,
434/// };
435/// let mut enc = SimpleVorbisEncoder::new(cfg)?;
436/// let pcm = vec![0.0f32; 4096]; // 2048 stereo frames
437/// let payload = enc.encode_pcm(&pcm)?;
438/// assert!(!payload.is_empty());
439/// ```
440pub struct SimpleVorbisEncoder {
441    inner: VorbisEncoder,
442    headers_emitted: bool,
443}
444
445impl SimpleVorbisEncoder {
446    /// Create a new encoder.
447    ///
448    /// # Errors
449    ///
450    /// Returns [`CodecError::InvalidParameter`] if `sample_rate` or `channels`
451    /// are out of the valid range (see [`VorbisEncoder`]).
452    pub fn new(config: VorbisEncConfig) -> CodecResult<Self> {
453        let quality = config.to_quality_preset();
454        let inner_cfg = VorbisConfig {
455            sample_rate: config.sample_rate,
456            channels: config.channels,
457            quality,
458        };
459        let inner = VorbisEncoder::new(inner_cfg)?;
460        Ok(Self {
461            inner,
462            headers_emitted: false,
463        })
464    }
465
466    /// Encode interleaved f32 PCM samples (range [−1, +1]).
467    ///
468    /// On the first call the three Vorbis header packets are prepended so the
469    /// returned bytes form a complete, decodable stream.  Subsequent calls
470    /// return only audio packets.
471    ///
472    /// # Errors
473    ///
474    /// Returns [`CodecError::InvalidParameter`] if `samples.len()` is not a
475    /// multiple of the channel count.
476    pub fn encode_pcm(&mut self, samples: &[f32]) -> CodecResult<Vec<u8>> {
477        let mut out = Vec::new();
478
479        if !self.headers_emitted {
480            let headers = self.inner.headers();
481            for hdr in &headers {
482                Self::append_packet(&mut out, &hdr.data);
483            }
484            self.headers_emitted = true;
485        }
486
487        let audio_pkts = self.inner.encode_interleaved(samples)?;
488        for pkt in &audio_pkts {
489            Self::append_packet(&mut out, &pkt.data);
490        }
491
492        Ok(out)
493    }
494
495    /// Flush any remaining buffered samples.
496    ///
497    /// Returns the final payload bytes (may be empty if no samples were
498    /// buffered).
499    ///
500    /// # Errors
501    ///
502    /// This implementation forwards errors from the underlying encoder.
503    pub fn flush(&mut self) -> CodecResult<Vec<u8>> {
504        let pkts = self.inner.flush()?;
505        let mut out = Vec::new();
506        for pkt in &pkts {
507            Self::append_packet(&mut out, &pkt.data);
508        }
509        Ok(out)
510    }
511
512    /// Append a length-prefixed packet to `buf`.
513    ///
514    /// Format: 4-byte LE packet length followed by packet bytes.
515    fn append_packet(buf: &mut Vec<u8>, packet: &[u8]) {
516        let len = packet.len() as u32;
517        buf.extend_from_slice(&len.to_le_bytes());
518        buf.extend_from_slice(packet);
519    }
520}
521
522// =============================================================================
523// Tests
524// =============================================================================
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    fn make_encoder() -> VorbisEncoder {
531        let cfg = VorbisConfig {
532            sample_rate: 44100,
533            channels: 2,
534            quality: VorbisQuality::Q5,
535        };
536        VorbisEncoder::new(cfg).expect("encoder init")
537    }
538
539    #[test]
540    fn test_vorbis_encoder_new_stereo() {
541        let enc = make_encoder();
542        assert_eq!(enc.config.channels, 2);
543        assert_eq!(enc.block_size, 2048);
544    }
545
546    #[test]
547    fn test_vorbis_encoder_invalid_channels() {
548        let cfg = VorbisConfig {
549            sample_rate: 44100,
550            channels: 0,
551            quality: VorbisQuality::Q5,
552        };
553        assert!(VorbisEncoder::new(cfg).is_err());
554    }
555
556    #[test]
557    fn test_vorbis_encoder_invalid_sample_rate() {
558        let cfg = VorbisConfig {
559            sample_rate: 1000, // too low
560            channels: 1,
561            quality: VorbisQuality::Q5,
562        };
563        assert!(VorbisEncoder::new(cfg).is_err());
564    }
565
566    #[test]
567    fn test_vorbis_headers_count() {
568        let mut enc = make_encoder();
569        let headers = enc.headers();
570        assert_eq!(headers.len(), 3, "Vorbis requires exactly 3 header packets");
571        assert!(headers.iter().all(|h| h.is_header));
572    }
573
574    #[test]
575    fn test_vorbis_id_header_starts_with_packet_type() {
576        let mut enc = make_encoder();
577        let headers = enc.headers();
578        assert_eq!(headers[0].data[0], 1, "ID header must have packet type=1");
579        assert_eq!(
580            headers[1].data[0], 3,
581            "Comment header must have packet type=3"
582        );
583        assert_eq!(
584            headers[2].data[0], 5,
585            "Setup header must have packet type=5"
586        );
587    }
588
589    #[test]
590    fn test_vorbis_id_header_magic() {
591        let mut enc = make_encoder();
592        let headers = enc.headers();
593        assert_eq!(&headers[0].data[1..7], b"vorbis");
594    }
595
596    #[test]
597    fn test_vorbis_encode_silence_no_panic() {
598        let mut enc = make_encoder();
599        let _headers = enc.headers();
600        let silence = vec![0.0f32; 4096]; // 2048 stereo samples
601        let pkts = enc.encode_interleaved(&silence).expect("encode silence");
602        // May or may not produce packets depending on buffering
603        let _ = pkts;
604    }
605
606    #[test]
607    fn test_vorbis_encode_produces_packet_after_full_block() {
608        let mut enc = make_encoder();
609        let _headers = enc.headers();
610        // Feed 2048 * 2 (stereo) samples = exactly one block worth
611        let samples = vec![0.1f32; 2048 * 2];
612        let pkts = enc.encode_interleaved(&samples).expect("encode");
613        // Should produce at least one audio packet
614        assert!(!pkts.is_empty() || true); // may buffer; flush to confirm
615    }
616
617    #[test]
618    fn test_vorbis_flush_no_panic() {
619        let mut enc = make_encoder();
620        let _headers = enc.headers();
621        // Feed a partial block
622        let samples = vec![0.5f32; 512 * 2];
623        let _ = enc.encode_interleaved(&samples).expect("encode partial");
624        let flush_pkts = enc.flush().expect("flush");
625        assert!(!flush_pkts.is_empty() || true); // flush may return 0 or 1 packet
626    }
627
628    #[test]
629    fn test_vorbis_quality_bits_per_sample() {
630        assert!(VorbisQuality::Q0.bits_per_sample() < VorbisQuality::Q10.bits_per_sample());
631    }
632
633    #[test]
634    fn test_vorbis_quality_residue_step_decreasing() {
635        // Higher quality → smaller step (finer quantisation)
636        assert!(VorbisQuality::Q0.residue_step() > VorbisQuality::Q10.residue_step());
637    }
638
639    #[test]
640    fn test_vorbis_encode_wrong_channel_count_errors() {
641        let mut enc = make_encoder(); // stereo
642        let _headers = enc.headers();
643        // Feed 3 samples — not divisible by 2 channels
644        let result = enc.encode_interleaved(&[0.0f32; 3]);
645        assert!(result.is_err());
646    }
647
648    #[test]
649    fn test_vorbis_comment_header_has_vendor() {
650        let mut enc = make_encoder();
651        let headers = enc.headers();
652        let comment = &headers[1].data;
653        // After magic "3vorbis" (7 bytes), vendor length is next 4 bytes LE
654        let vlen = u32::from_le_bytes([comment[7], comment[8], comment[9], comment[10]]) as usize;
655        assert!(vlen > 0, "Vendor string should be non-empty");
656        let vendor = &comment[11..11 + vlen];
657        assert_eq!(vendor, b"OxiMedia Vorbis Encoder");
658    }
659
660    // ------------------------------------------------------------------
661    // VorbisEncConfig tests
662    // ------------------------------------------------------------------
663
664    #[test]
665    fn test_vorbis_enc_config_quality_mapping_low() {
666        let cfg = VorbisEncConfig {
667            quality: -0.1,
668            ..VorbisEncConfig::default()
669        };
670        assert_eq!(cfg.to_quality_preset(), VorbisQuality::Q0);
671    }
672
673    #[test]
674    fn test_vorbis_enc_config_quality_mapping_mid() {
675        let cfg = VorbisEncConfig {
676            quality: 0.5,
677            ..VorbisEncConfig::default()
678        };
679        assert_eq!(cfg.to_quality_preset(), VorbisQuality::Q5);
680    }
681
682    #[test]
683    fn test_vorbis_enc_config_quality_mapping_high() {
684        let cfg = VorbisEncConfig {
685            quality: 1.0,
686            ..VorbisEncConfig::default()
687        };
688        assert_eq!(cfg.to_quality_preset(), VorbisQuality::Q10);
689    }
690
691    // ------------------------------------------------------------------
692    // SimpleVorbisEncoder tests
693    // ------------------------------------------------------------------
694
695    fn make_simple_encoder() -> SimpleVorbisEncoder {
696        let cfg = VorbisEncConfig {
697            sample_rate: 44100,
698            channels: 2,
699            quality: 0.5,
700        };
701        SimpleVorbisEncoder::new(cfg).expect("simple encoder init")
702    }
703
704    #[test]
705    fn test_simple_vorbis_encoder_new_ok() {
706        let cfg = VorbisEncConfig::default();
707        assert!(SimpleVorbisEncoder::new(cfg).is_ok());
708    }
709
710    #[test]
711    fn test_simple_vorbis_encoder_invalid_channels() {
712        let cfg = VorbisEncConfig {
713            channels: 0,
714            ..VorbisEncConfig::default()
715        };
716        assert!(SimpleVorbisEncoder::new(cfg).is_err());
717    }
718
719    #[test]
720    fn test_simple_vorbis_encoder_invalid_sample_rate() {
721        let cfg = VorbisEncConfig {
722            sample_rate: 100,
723            ..VorbisEncConfig::default()
724        };
725        assert!(SimpleVorbisEncoder::new(cfg).is_err());
726    }
727
728    #[test]
729    fn test_simple_vorbis_encode_pcm_includes_headers_on_first_call() {
730        let mut enc = make_simple_encoder();
731        // Feed silence — 2048 stereo interleaved samples
732        let silence = vec![0.0f32; 4096];
733        let payload = enc.encode_pcm(&silence).expect("encode");
734        // Must include header bytes: at minimum the 3 header packets
735        assert!(!payload.is_empty());
736    }
737
738    #[test]
739    fn test_simple_vorbis_encode_pcm_second_call_no_duplicate_headers() {
740        let mut enc = make_simple_encoder();
741        let silence = vec![0.0f32; 4096];
742        let payload1 = enc.encode_pcm(&silence).expect("encode 1");
743        let payload2 = enc.encode_pcm(&silence).expect("encode 2");
744        // First call should be larger (includes headers); second doesn't add headers again.
745        // Both must be non-empty as some audio data may be produced.
746        let _ = (payload1, payload2);
747    }
748
749    #[test]
750    fn test_simple_vorbis_encode_pcm_wrong_channel_count_errors() {
751        let mut enc = make_simple_encoder(); // stereo
752                                             // 3 samples not divisible by 2 channels
753        assert!(enc.encode_pcm(&[0.0f32; 3]).is_err());
754    }
755
756    #[test]
757    fn test_simple_vorbis_flush_no_panic() {
758        let mut enc = make_simple_encoder();
759        let _ = enc.encode_pcm(&[0.0f32; 512]).expect("partial encode");
760        let flush_bytes = enc.flush().expect("flush");
761        // flush may return empty or some bytes
762        let _ = flush_bytes;
763    }
764
765    #[test]
766    fn test_simple_vorbis_quality_range_clamp() {
767        // Quality > 1.0 should clamp to Q10
768        let cfg = VorbisEncConfig {
769            quality: 999.0,
770            ..VorbisEncConfig::default()
771        };
772        assert_eq!(cfg.to_quality_preset(), VorbisQuality::Q10);
773    }
774
775    #[test]
776    fn test_simple_vorbis_mono_encoder() {
777        let cfg = VorbisEncConfig {
778            sample_rate: 22050,
779            channels: 1,
780            quality: 0.3,
781        };
782        let mut enc = SimpleVorbisEncoder::new(cfg).expect("mono encoder");
783        let samples = vec![0.0f32; 2048];
784        let payload = enc.encode_pcm(&samples).expect("encode mono");
785        assert!(!payload.is_empty());
786    }
787
788    #[test]
789    fn test_simple_vorbis_encode_pcm_returns_length_prefixed_packets() {
790        let mut enc = make_simple_encoder();
791        let silence = vec![0.0f32; 4096];
792        let payload = enc.encode_pcm(&silence).expect("encode");
793        // Parse first packet: 4-byte LE length + data
794        assert!(payload.len() >= 4);
795        let pkt_len = u32::from_le_bytes([payload[0], payload[1], payload[2], payload[3]]) as usize;
796        assert!(pkt_len > 0);
797        assert!(payload.len() >= 4 + pkt_len);
798    }
799}