Skip to main content

siglus_scene_vm/audio/
nwa_stream.rs

1//! NWA music decoded while it plays: a Kira streaming decoder over an
2//! `NwaReader`, so a track costs one compressed unit in memory instead of
3//! its whole PCM (up to ~50 MB for a long BGM).
4
5use std::path::Path;
6
7use kira::Frame;
8use kira::sound::FromFileError;
9use kira::sound::streaming::Decoder;
10use siglus_assets::nwa::NwaReader;
11
12/// Frames decoded per request from the streaming thread.
13const CHUNK_FRAMES: u32 = 4096;
14
15pub struct NwaDecoder {
16    reader: NwaReader,
17    channels: u16,
18    bits: u16,
19    sample_rate: u32,
20    frames: usize,
21}
22
23fn io_error(error: anyhow::Error) -> FromFileError {
24    FromFileError::IoError(std::io::Error::other(format!("{error:#}")))
25}
26
27impl NwaDecoder {
28    pub fn open(path: &Path) -> Result<Self, FromFileError> {
29        let reader = NwaReader::open(path).map_err(io_error)?;
30        let header = reader.header();
31        let (channels, bits) = (header.channels, header.bits_per_sample);
32        if !matches!(channels, 1 | 2) {
33            return Err(FromFileError::UnsupportedChannelConfiguration);
34        }
35        if !matches!(bits, 8 | 16) {
36            return Err(io_error(anyhow::anyhow!("NWA: {bits}-bit samples")));
37        }
38        Ok(Self {
39            channels,
40            bits,
41            sample_rate: header.samples_per_sec.max(1),
42            frames: header.frame_count() as usize,
43            reader,
44        })
45    }
46}
47
48impl Decoder for NwaDecoder {
49    type Error = FromFileError;
50
51    fn sample_rate(&self) -> u32 {
52        self.sample_rate
53    }
54
55    fn num_frames(&self) -> usize {
56        self.frames
57    }
58
59    fn decode(&mut self) -> Result<Vec<Frame>, Self::Error> {
60        let bytes = self.reader.read_samples(CHUNK_FRAMES).map_err(io_error)?;
61        let sample = |i: usize| -> f32 {
62            if self.bits == 16 {
63                f32::from(i16::from_le_bytes([bytes[i * 2], bytes[i * 2 + 1]])) / 32768.0
64            } else {
65                (f32::from(bytes[i]) - 128.0) / 128.0
66            }
67        };
68        let width = usize::from(self.bits / 8) * usize::from(self.channels);
69        let count = bytes.len() / width;
70        let frames = (0..count)
71            .map(|f| {
72                if self.channels == 2 {
73                    Frame::new(sample(f * 2), sample(f * 2 + 1))
74                } else {
75                    Frame::from_mono(sample(f))
76                }
77            })
78            .collect();
79        Ok(frames)
80    }
81
82    fn seek(&mut self, index: usize) -> Result<usize, Self::Error> {
83        let index = index.min(self.frames);
84        self.reader.set_read_sample_pos(index as u32);
85        Ok(index)
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    /// Streams a real NWA (`SIGLUS_TEST_NWA`, or RewriteHF's smallest BGM
94    /// when present) and compares it with the whole-file decode.
95    #[test]
96    fn streamed_samples_match_the_full_decode() {
97        let path = std::env::var_os("SIGLUS_TEST_NWA")
98            .map(std::path::PathBuf::from)
99            .unwrap_or_else(|| "/Users/xmoe/Documents/RewriteHF/bgm/BGM001.nwa".into());
100        if !path.is_file() {
101            return;
102        }
103        let mut decoder = NwaDecoder::open(&path).unwrap();
104        let wav = NwaReader::open(&path).unwrap().to_wav_bytes().unwrap();
105        let pcm = &wav[44..];
106        let mut streamed = Vec::new();
107        loop {
108            let frames = decoder.decode().unwrap();
109            if frames.is_empty() {
110                break;
111            }
112            streamed.extend(frames);
113        }
114        assert_eq!(streamed.len(), decoder.num_frames());
115        for (i, frame) in streamed.iter().enumerate().step_by(997) {
116            let left = i16::from_le_bytes([pcm[i * 4], pcm[i * 4 + 1]]);
117            assert_eq!(frame.left, f32::from(left) / 32768.0, "frame {i}");
118        }
119        // Seeking lands where asked.
120        let middle = decoder.num_frames() / 2;
121        assert_eq!(decoder.seek(middle).unwrap(), middle);
122        let frame = decoder.decode().unwrap()[0];
123        let left = i16::from_le_bytes([pcm[middle * 4], pcm[middle * 4 + 1]]);
124        assert_eq!(frame.left, f32::from(left) / 32768.0);
125    }
126}