use super::{
asset_handler::{AssetHandler, BakeContext, LoadErrors},
audio_utils::{bit_depth_from_bits_per_sample, sample_count_from_pcm_len},
ResourceId,
};
use crate::{processors::audio_processor::process_audio_in, resources::audio::Audio};
impl WAVAssetHandler {
fn decode_wav(data: &[u8]) -> Result<(Audio, &[u8]), String> {
fn read_u16(bytes: &[u8], offset: usize, name: &str) -> Result<u16, String> {
let bytes = bytes
.get(offset..offset + 2)
.ok_or_else(|| format!("Invalid {name}. The WAV file is likely truncated."))?;
Ok(u16::from_le_bytes([bytes[0], bytes[1]]))
}
fn read_u32(bytes: &[u8], offset: usize, name: &str) -> Result<u32, String> {
let bytes = bytes
.get(offset..offset + 4)
.ok_or_else(|| format!("Invalid {name}. The WAV file is likely truncated."))?;
Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}
let riff = data
.get(0..4)
.ok_or_else(|| "Invalid RIFF header. The file is likely truncated or not a WAV asset.".to_string())?;
if riff != b"RIFF" {
return Err("Invalid RIFF header. The file is likely not a WAV asset.".to_string());
}
let format = data
.get(8..12)
.ok_or_else(|| "Invalid WAVE format. The file is likely truncated or not a WAV asset.".to_string())?;
if format != b"WAVE" {
return Err("Invalid WAVE format. The file is likely not a WAV asset.".to_string());
}
let mut offset = 12;
let mut wav_format = None;
let mut pcm_data = None;
while offset + 8 <= data.len() {
let chunk_id = data.get(offset..offset + 4).expect("chunk header was bounds checked");
let chunk_size = read_u32(data, offset + 4, "chunk size")? as usize;
let chunk_start = offset + 8;
let chunk_end = chunk_start
.checked_add(chunk_size)
.ok_or_else(|| "Invalid chunk size. The WAV chunk size likely overflowed.".to_string())?;
let chunk = data
.get(chunk_start..chunk_end)
.ok_or_else(|| "Invalid chunk data. The WAV file is likely truncated.".to_string())?;
match chunk_id {
b"fmt " => {
if chunk_size < 16 {
return Err("Invalid fmt chunk. The WAV format chunk is likely incomplete.".to_string());
}
let audio_format = read_u16(chunk, 0, "audio format")?;
let num_channels = read_u16(chunk, 2, "channel count")?;
let sample_rate = read_u32(chunk, 4, "sample rate")?;
let bits_per_sample = read_u16(chunk, 14, "bits per sample")?;
let is_pcm = match audio_format {
0x0001 => true,
0xfffe => {
if chunk_size < 40 {
return Err(
"Invalid extensible fmt chunk. The WAV format chunk is likely incomplete.".to_string()
);
}
let sub_format = chunk.get(24..40).ok_or_else(|| {
"Invalid extensible format. The WAV format chunk is likely incomplete.".to_string()
})?;
sub_format
== [
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b,
0x71,
]
}
_ => false,
};
if !is_pcm {
return Err("Unsupported audio format. The WAV file is likely not PCM encoded.".to_string());
}
if num_channels != 1 && num_channels != 2 {
return Err(
"Unsupported channel count. The WAV header likely reports an unsupported configuration."
.to_string(),
);
}
let bit_depth = bit_depth_from_bits_per_sample(bits_per_sample).ok_or_else(|| {
"Unsupported bit depth. The WAV header likely reports an unsupported format.".to_string()
})?;
wav_format = Some((bit_depth, num_channels, sample_rate, bits_per_sample));
}
b"data" => pcm_data = Some(chunk),
_ => {}
}
offset = chunk_end + (chunk_size % 2);
}
let (bit_depth, num_channels, sample_rate) = wav_format
.map(|(bit_depth, num_channels, sample_rate, _bits_per_sample)| (bit_depth, num_channels, sample_rate))
.ok_or_else(|| "Missing fmt chunk. The WAV file likely has no format description.".to_string())?;
let data = pcm_data.ok_or_else(|| "Missing data chunk. The WAV file likely has no PCM payload.".to_string())?;
let sample_count = sample_count_from_pcm_len(data.len(), num_channels, bit_depth);
let audio_resource = Audio {
bit_depth,
channel_count: num_channels,
sample_rate,
sample_count,
};
Ok((audio_resource, data))
}
pub fn new() -> WAVAssetHandler {
Self::default()
}
}
#[derive(Default)]
pub struct WAVAssetHandler {}
impl AssetHandler for WAVAssetHandler {
fn can_handle(&self, r#type: &str) -> bool {
r#type == "wav"
}
async fn bake<'a>(&'a self, context: BakeContext<'a>, url: ResourceId<'a>) -> Result<(), LoadErrors> {
if let Some(dt) = context.resource_type(url) {
if !self.can_handle(dt) {
return Err(LoadErrors::UnsupportedType);
}
}
let (data, _, dt) = context.resolve(url).await?;
let allocator = context.allocator();
if !self.can_handle(&dt) {
return Err(LoadErrors::UnsupportedType);
}
let (audio_resource, pcm_data) = Self::decode_wav(&data).map_err(|_| LoadErrors::FailedToProcess)?;
let mut output = Vec::with_capacity_in(pcm_data.len(), allocator);
output.extend_from_slice(pcm_data);
let (asset, data) = process_audio_in(url, audio_resource, output.into_boxed_slice())?;
context.store_primary(asset, &data)
}
}
#[cfg(test)]
mod tests {
use crate::{
asset::{self, asset_manager::AssetManager, wav_asset_handler::WAVAssetHandler, ResourceId},
r#async, resource,
resources::audio::Audio,
types::BitDepths,
};
fn chunk(id: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut chunk = Vec::new();
chunk.extend_from_slice(id);
chunk.extend_from_slice(&(payload.len() as u32).to_le_bytes());
chunk.extend_from_slice(payload);
if payload.len() % 2 == 1 {
chunk.push(0);
}
chunk
}
fn riff(chunks: &[Vec<u8>]) -> Vec<u8> {
let mut wav = Vec::new();
wav.extend_from_slice(b"RIFF");
let size = 4 + chunks.iter().map(Vec::len).sum::<usize>() as u32;
wav.extend_from_slice(&size.to_le_bytes());
wav.extend_from_slice(b"WAVE");
for chunk in chunks {
wav.extend_from_slice(chunk);
}
wav
}
fn pcm_fmt() -> Vec<u8> {
let mut fmt = Vec::new();
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&2u16.to_le_bytes());
fmt.extend_from_slice(&44_100u32.to_le_bytes());
fmt.extend_from_slice(&176_400u32.to_le_bytes());
fmt.extend_from_slice(&4u16.to_le_bytes());
fmt.extend_from_slice(&16u16.to_le_bytes());
fmt
}
fn mono_pcm_fmt() -> Vec<u8> {
let mut fmt = Vec::new();
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&1u16.to_le_bytes());
fmt.extend_from_slice(&48_000u32.to_le_bytes());
fmt.extend_from_slice(&96_000u32.to_le_bytes());
fmt.extend_from_slice(&2u16.to_le_bytes());
fmt.extend_from_slice(&16u16.to_le_bytes());
fmt
}
#[test]
fn decode_wav_skips_extra_metadata_chunks() {
let pcm = [1, 2, 3, 4, 5, 6, 7, 8];
let wav = riff(&[
chunk(b"JUNK", b"abc"),
chunk(b"fmt ", &pcm_fmt()),
chunk(b"LIST", b"INFOISFTByte Engine"),
chunk(b"data", &pcm),
]);
let (audio, data) = WAVAssetHandler::decode_wav(&wav).expect("WAV should decode");
assert_eq!(audio.bit_depth, BitDepths::Sixteen);
assert_eq!(audio.channel_count, 2);
assert_eq!(audio.sample_rate, 44_100);
assert_eq!(audio.sample_count, 2);
assert_eq!(data, pcm);
}
#[test]
fn decode_wav_allows_reordered_chunks_and_extensible_pcm() {
let pcm = [1, 2, 3, 4];
let mut fmt = pcm_fmt();
fmt[0..2].copy_from_slice(&0xfffeu16.to_le_bytes());
fmt.extend_from_slice(&22u16.to_le_bytes());
fmt.extend_from_slice(&16u16.to_le_bytes());
fmt.extend_from_slice(&3u32.to_le_bytes());
fmt.extend_from_slice(&[
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71,
]);
let wav = riff(&[chunk(b"data", &pcm), chunk(b"fmt ", &fmt)]);
let (audio, data) = WAVAssetHandler::decode_wav(&wav).expect("WAV should decode");
assert_eq!(audio.bit_depth, BitDepths::Sixteen);
assert_eq!(audio.sample_count, 1);
assert_eq!(data, pcm);
}
#[r#async::test]
async fn asset_manager_bakes_generated_wav() {
let pcm = [0x00, 0x80, 0xff, 0x7f];
let wav = riff(&[chunk(b"fmt ", &mono_pcm_fmt()), chunk(b"data", &pcm)]);
let asset_storage_backend = asset::storage_backend::tests::TestStorageBackend::new();
let resource_storage_backend = resource::storage_backend::tests::TestStorageBackend::new();
asset_storage_backend.add_file("generated.wav", &wav);
let mut asset_manager = AssetManager::new(asset_storage_backend);
asset_manager.add_asset_handler(WAVAssetHandler::new());
asset_manager
.bake("generated.wav", &resource_storage_backend)
.await
.expect("generated WAV should bake");
let resource = resource_storage_backend
.get_resource(ResourceId::new("generated.wav"))
.expect("baked WAV resource should be stored");
assert_eq!(resource.class, "Audio");
let resource: Audio = crate::from_slice(&resource.resource).unwrap();
assert_eq!(resource.bit_depth, BitDepths::Sixteen);
assert_eq!(resource.channel_count, 1);
assert_eq!(resource.sample_rate, 48_000);
assert_eq!(resource.sample_count, 2);
assert_eq!(
resource_storage_backend
.get_resource_data_by_name(ResourceId::new("generated.wav"))
.expect("baked WAV samples should be stored")
.as_ref(),
pcm
);
}
}