siglus_scene_vm/audio/
nwa_stream.rs1use std::path::Path;
6
7use kira::Frame;
8use kira::sound::FromFileError;
9use kira::sound::streaming::Decoder;
10use siglus_assets::nwa::NwaReader;
11
12const 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 #[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 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}