Skip to main content

siglus_assets/
vorbis.rs

1//! Ogg/Vorbis decoding helpers.
2//!
3//! Siglus resources commonly embed sound as Ogg/Vorbis (sometimes inside OVK or
4//! with a simple XOR obfuscation (OWP)). This module provides a small,
5//! format-oriented API that decodes to interleaved PCM16.
6
7use anyhow::{anyhow, Result};
8use std::io::{Read, Seek};
9
10#[derive(Debug, Clone)]
11pub struct Pcm16 {
12    /// Number of audio channels.
13    pub channels: u16,
14    /// Sample rate (Hz).
15    pub sample_rate: u32,
16    /// Interleaved signed PCM16 samples.
17    pub samples: Vec<i16>,
18}
19
20/// Decode an Ogg/Vorbis stream into interleaved PCM16.
21///
22/// This uses `lewton::inside_ogg::OggStreamReader`, which expects a *pure audio*
23/// Ogg/Vorbis stream (i.e., not OGV with multiple streams). This matches the
24/// typical Siglus usage (BGM/SE stored as .ogg/.owp or embedded in OVK).
25pub fn decode_ogg_vorbis_reader<T: Read + Seek>(rdr: T) -> Result<Pcm16> {
26    use lewton::inside_ogg::OggStreamReader;
27
28    let mut r = OggStreamReader::new(rdr)
29        .map_err(|e| anyhow!("ogg/vorbis: failed to parse headers: {e}"))?;
30
31    let channels = r.ident_hdr.audio_channels as u16;
32    let sample_rate = r.ident_hdr.audio_sample_rate;
33
34    let mut samples = Vec::<i16>::new();
35    while let Some(pkt) = r
36        .read_dec_packet_itl()
37        .map_err(|e| anyhow!("ogg/vorbis: decode error: {e}"))?
38    {
39        samples.extend_from_slice(&pkt);
40    }
41
42    Ok(Pcm16 {
43        channels,
44        sample_rate,
45        samples,
46    })
47}
48
49/// Decode an Ogg/Vorbis blob in memory.
50pub fn decode_ogg_vorbis_bytes(data: &[u8]) -> Result<Pcm16> {
51    decode_ogg_vorbis_reader(std::io::Cursor::new(data))
52}
53
54/// Encode PCM16 into a minimal RIFF/WAVE (PCM) buffer.
55///
56/// This is intended as a convenience for exporting assets.
57pub fn pcm16_to_wav_bytes(pcm: &Pcm16) -> Vec<u8> {
58    // RIFF/WAVE (PCM) header: 44 bytes.
59    let num_channels = pcm.channels as u16;
60    let sample_rate = pcm.sample_rate as u32;
61    let bits_per_sample: u16 = 16;
62    let block_align: u16 = num_channels.saturating_mul(bits_per_sample / 8);
63    let byte_rate: u32 = sample_rate.saturating_mul(block_align as u32);
64
65    let data_bytes_len: u32 = (pcm.samples.len() * 2) as u32;
66    let riff_size: u32 = 36u32.saturating_add(data_bytes_len);
67
68    let mut out = Vec::with_capacity(44 + data_bytes_len as usize);
69
70    // RIFF header
71    out.extend_from_slice(b"RIFF");
72    out.extend_from_slice(&riff_size.to_le_bytes());
73    out.extend_from_slice(b"WAVE");
74
75    // fmt chunk
76    out.extend_from_slice(b"fmt ");
77    out.extend_from_slice(&16u32.to_le_bytes()); // PCM fmt chunk size
78    out.extend_from_slice(&1u16.to_le_bytes()); // format tag: PCM
79    out.extend_from_slice(&num_channels.to_le_bytes());
80    out.extend_from_slice(&sample_rate.to_le_bytes());
81    out.extend_from_slice(&byte_rate.to_le_bytes());
82    out.extend_from_slice(&block_align.to_le_bytes());
83    out.extend_from_slice(&bits_per_sample.to_le_bytes());
84
85    // data chunk
86    out.extend_from_slice(b"data");
87    out.extend_from_slice(&data_bytes_len.to_le_bytes());
88
89    // PCM payload
90    for s in &pcm.samples {
91        out.extend_from_slice(&s.to_le_bytes());
92    }
93
94    out
95}
96
97/// Decode Ogg/Vorbis bytes and immediately return WAV bytes.
98pub fn decode_ogg_vorbis_bytes_to_wav(data: &[u8]) -> Result<Vec<u8>> {
99    let pcm = decode_ogg_vorbis_bytes(data)?;
100    Ok(pcm16_to_wav_bytes(&pcm))
101}
102
103/// Decode an Ogg/Vorbis stream and immediately return WAV bytes.
104pub fn decode_ogg_vorbis_reader_to_wav<T: Read + Seek>(rdr: T) -> Result<Vec<u8>> {
105    let pcm = decode_ogg_vorbis_reader(rdr)?;
106    Ok(pcm16_to_wav_bytes(&pcm))
107}