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::{Result, anyhow};
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/// What `decode_ogg_vorbis_reader` would produce, without keeping it:
50/// channels, sample rate and the number of interleaved samples. For timing
51/// a sound that plays as a stream by its decoded length.
52pub fn ogg_vorbis_decoded_len<T: Read + Seek>(rdr: T) -> Result<(u16, u32, u64)> {
53    use lewton::inside_ogg::OggStreamReader;
54
55    let mut r = OggStreamReader::new(rdr)
56        .map_err(|e| anyhow!("ogg/vorbis: failed to parse headers: {e}"))?;
57    let channels = r.ident_hdr.audio_channels as u16;
58    let sample_rate = r.ident_hdr.audio_sample_rate;
59    let mut samples = 0u64;
60    while let Some(pkt) = r
61        .read_dec_packet_itl()
62        .map_err(|e| anyhow!("ogg/vorbis: decode error: {e}"))?
63    {
64        samples += pkt.len() as u64;
65    }
66    Ok((channels, sample_rate, samples))
67}
68
69/// Decode an Ogg/Vorbis blob in memory.
70pub fn decode_ogg_vorbis_bytes(data: &[u8]) -> Result<Pcm16> {
71    decode_ogg_vorbis_reader(std::io::Cursor::new(data))
72}
73
74/// Encode PCM16 into a minimal RIFF/WAVE (PCM) buffer.
75///
76/// This is intended as a convenience for exporting assets.
77pub fn pcm16_to_wav_bytes(pcm: &Pcm16) -> Vec<u8> {
78    // RIFF/WAVE (PCM) header: 44 bytes.
79    let num_channels = pcm.channels;
80    let sample_rate = pcm.sample_rate;
81    let bits_per_sample: u16 = 16;
82    let block_align: u16 = num_channels.saturating_mul(bits_per_sample / 8);
83    let byte_rate: u32 = sample_rate.saturating_mul(block_align as u32);
84
85    let data_bytes_len: u32 = (pcm.samples.len() * 2) as u32;
86    let riff_size: u32 = 36u32.saturating_add(data_bytes_len);
87
88    let mut out = Vec::with_capacity(44 + data_bytes_len as usize);
89
90    // RIFF header
91    out.extend_from_slice(b"RIFF");
92    out.extend_from_slice(&riff_size.to_le_bytes());
93    out.extend_from_slice(b"WAVE");
94
95    // fmt chunk
96    out.extend_from_slice(b"fmt ");
97    out.extend_from_slice(&16u32.to_le_bytes()); // PCM fmt chunk size
98    out.extend_from_slice(&1u16.to_le_bytes()); // format tag: PCM
99    out.extend_from_slice(&num_channels.to_le_bytes());
100    out.extend_from_slice(&sample_rate.to_le_bytes());
101    out.extend_from_slice(&byte_rate.to_le_bytes());
102    out.extend_from_slice(&block_align.to_le_bytes());
103    out.extend_from_slice(&bits_per_sample.to_le_bytes());
104
105    // data chunk
106    out.extend_from_slice(b"data");
107    out.extend_from_slice(&data_bytes_len.to_le_bytes());
108
109    // PCM payload
110    for s in &pcm.samples {
111        out.extend_from_slice(&s.to_le_bytes());
112    }
113
114    out
115}
116
117/// Decode Ogg/Vorbis bytes and immediately return WAV bytes.
118pub fn decode_ogg_vorbis_bytes_to_wav(data: &[u8]) -> Result<Vec<u8>> {
119    let pcm = decode_ogg_vorbis_bytes(data)?;
120    Ok(pcm16_to_wav_bytes(&pcm))
121}
122
123/// Decode an Ogg/Vorbis stream and immediately return WAV bytes.
124pub fn decode_ogg_vorbis_reader_to_wav<T: Read + Seek>(rdr: T) -> Result<Vec<u8>> {
125    let pcm = decode_ogg_vorbis_reader(rdr)?;
126    Ok(pcm16_to_wav_bytes(&pcm))
127}