Skip to main content

khora_io/asset/decoders/audio/
wav.rs

1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! `.wav` audio decoder.
16
17use anyhow::{anyhow, Result};
18use khora_data::assets::SoundData;
19use std::{error::Error, io::Cursor};
20
21use crate::asset::AssetDecoder;
22
23/// Decodes audio from the WAV format using `hound`.
24#[derive(Default)]
25pub struct WavDecoder;
26
27impl WavDecoder {
28    /// Creates a new instance of `WavDecoder`.
29    pub fn new() -> Self {
30        Self
31    }
32}
33
34impl AssetDecoder<SoundData> for WavDecoder {
35    fn load(&self, bytes: &[u8]) -> Result<SoundData, Box<dyn Error + Send + Sync>> {
36        let cursor = Cursor::new(bytes);
37        let mut reader = hound::WavReader::new(cursor)?;
38
39        let spec = reader.spec();
40
41        let samples: Result<Vec<f32>, _> = match spec.sample_format {
42            hound::SampleFormat::Float => reader.samples::<f32>().collect(),
43            hound::SampleFormat::Int => {
44                let max_value = (1 << (spec.bits_per_sample - 1)) as f32;
45                reader
46                    .samples::<i32>()
47                    .map(|sample| sample.map(|s| s as f32 / max_value))
48                    .collect()
49            }
50        };
51
52        let samples = samples.map_err(|e| anyhow!("Failed to parse WAV samples: {}", e))?;
53
54        Ok(SoundData {
55            samples,
56            channels: spec.channels,
57            sample_rate: spec.sample_rate,
58        })
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    // 16-bit, mono, 44100Hz, 4 samples (0.1, -0.1, 0.2, -0.2).
67    const TEST_WAV_BYTES: &[u8] = &[
68        82, 73, 70, 70, 52, 0, 0, 0, 87, 65, 86, 69, 102, 109, 116, 32, 16, 0, 0, 0, 1, 0, 1, 0,
69        68, 172, 0, 0, 136, 88, 1, 0, 2, 0, 16, 0, 100, 97, 116, 97, 8, 0, 0, 0, 0, 12, 204, 251,
70        51, 13, 205, 243,
71    ];
72
73    #[test]
74    fn wav_loader_success() {
75        let loader = WavDecoder::new();
76        let result = loader.load(TEST_WAV_BYTES);
77        assert!(result.is_ok());
78        let sound_data = result.unwrap();
79        assert_eq!(sound_data.sample_rate, 44100);
80        assert_eq!(sound_data.channels, 1);
81        assert!(!sound_data.samples.is_empty());
82    }
83
84    #[test]
85    fn wav_loader_invalid_bytes() {
86        let loader = WavDecoder::new();
87        let invalid_bytes = &[0, 1, 2, 3, 4];
88        assert!(loader.load(invalid_bytes).is_err());
89    }
90}