From f9c0140100219d38d28457bc27abf1d3a095dabc Mon Sep 17 00:00:00 2001 From: Simon Bruder Date: Sun, 5 Jul 2020 21:17:42 +0200 Subject: [PATCH] Add documentation and tests for xact3::adpcm This also renames variables to be easier to understand (rather than copying the naming scheme of the spec) and moves the calculation of computable fields in the wave format header into the adpcm module. --- src/xact3.rs | 4 ++ src/xact3/adpcm.rs | 161 +++++++++++++++++++++++++++++++++++++-------- src/xact3/xwb.rs | 14 ++-- 3 files changed, 142 insertions(+), 37 deletions(-) diff --git a/src/xact3.rs b/src/xact3.rs index e697876..92cb284 100644 --- a/src/xact3.rs +++ b/src/xact3.rs @@ -1,2 +1,6 @@ +/// Module for writing raw ADPCM data to a RIFF WAVE file. +/// +/// The WAVE format for ADPCM is specified in the [Microsoft Multimedia Standards Update from 15 +/// April 1994](https://web.archive.org/web/20120917060438if_/http://download.microsoft.com/download/9/8/6/9863C72A-A3AA-4DDB-B1BA-CA8D17EFD2D4/RIFFNEW.pdf). mod adpcm; pub mod xwb; diff --git a/src/xact3/adpcm.rs b/src/xact3/adpcm.rs index 5931c53..538c301 100644 --- a/src/xact3/adpcm.rs +++ b/src/xact3/adpcm.rs @@ -5,6 +5,7 @@ use byteorder::{WriteBytesExt, LE}; use log::{debug, trace}; use thiserror::Error; +/// Standard ADPCM coefficients #[rustfmt::skip] const COEFFS: &[CoefSet] = &[ (256, 0), @@ -18,27 +19,41 @@ const COEFFS: &[CoefSet] = &[ #[derive(Debug, Error)] pub enum Error { - #[error("unable to create file of size {0} (larger than 2^32)")] - TooLarge(usize), + /// WAVE only supports file sizes up to 232 bytes (232 - 82 bytes of + /// usable audio data in this case). + #[error("unable to create file of size {0} (larger than 2^32 - 82 bytes)")] + TooLargeError(usize), } +/// All wave chunks implement this trait. trait WaveChunk { + /// Serialize to byte vector that is used as a part of the resulting wave file. fn to_chunk(&self) -> Vec; } +/// One set of ADPCM coefficients type CoefSet = (i16, i16); +/// `WAVE_FORMAT_ADPCM` header. +/// +/// It only includes fields that are usful for usage in conjunction with XACT3. The other fields +/// are static and defined in the [`to_chunk`] method of this type. +/// +/// [`to_chunk`]: trait.WaveChunk.html#tymethod.to_chunk pub struct WaveFormat { - // w_format_tag = 2 - pub n_channels: u16, - pub n_samples_per_sec: u32, - pub n_avg_bytes_per_sec: u32, - pub n_block_align: u16, - // w_bits_per_sample = 4 - // cb_size = 32 - pub n_samples_per_block: u16, - // w_num_coeff = 7 - // a_coeff = COEFFS + // wFormatTag = 2 + /// `nChannels`: Number of channels + pub channels: u16, + /// `nSamplesPerSec`: Sample rate + pub sample_rate: u32, + // nAvgBytesPerSec (calculated), + /// `nBlockAlign`: Block alignment (in bytes) + pub block_align: u16, + // wBitsPerSample = 4 + // cbSize = 32 + // nSamplesPerBlock (calculated) + // nNumCoeff = 7 + // aCoeff = COEFFS } impl WaveChunk for WaveFormat { @@ -48,14 +63,15 @@ impl WaveChunk for WaveFormat { buf.write_u32::(2 + 2 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 * COEFFS.len() as u32) .unwrap(); buf.write_u16::(2).unwrap(); // WAVE_FORMAT_ADPCM - buf.write_u16::(self.n_channels).unwrap(); - buf.write_u32::(self.n_samples_per_sec).unwrap(); - buf.write_u32::(self.n_avg_bytes_per_sec).unwrap(); - buf.write_u16::(self.n_block_align).unwrap(); + buf.write_u16::(self.channels).unwrap(); + buf.write_u32::(self.sample_rate).unwrap(); + buf.write_u32::(self.avg_bytes_per_sec()).unwrap(); // nAvgBytesPerSec + buf.write_u16::(self.block_align).unwrap(); buf.write_u16::(4).unwrap(); // wBitsPerSample buf.write_u16::(32).unwrap(); // cbSize - buf.write_u16::(self.n_samples_per_block).unwrap(); - buf.write_u16::(7).unwrap(); // wNumCoeff + buf.write_u16::(self.samples_per_block()).unwrap(); + buf.write_u16::(COEFFS.len().try_into().unwrap()) + .unwrap(); // nNumCoeff for coef_set in COEFFS { buf.write_i16::(coef_set.0).unwrap(); buf.write_i16::(coef_set.1).unwrap(); @@ -64,7 +80,21 @@ impl WaveChunk for WaveFormat { } } +impl WaveFormat { + /// Calculate `nSamplesPerBlock` + fn samples_per_block(&self) -> u16 { + (((self.block_align - (7 * self.channels)) * 8) / (4 * self.channels)) + 2 + } + + /// Calculate `nAvgBytesPerSec` + fn avg_bytes_per_sec(&self) -> u32 { + (self.sample_rate / u32::from(self.samples_per_block())) * u32::from(self.block_align) + } +} + +/// Wave fact chunk struct WaveFact { + /// The length of the audio data in samples length_samples: u32, } @@ -72,13 +102,15 @@ impl WaveChunk for WaveFact { fn to_chunk(&self) -> Vec { let mut buf = Cursor::new(Vec::new()); write!(buf, "fact").unwrap(); - buf.write_u32::(4).unwrap(); + buf.write_u32::(4).unwrap(); // length of fact chunk buf.write_u32::(self.length_samples).unwrap(); buf.into_inner() } } +/// RIFF header chunk struct RIFFHeader { + /// Size of the file minus 8 bytes (`RIFF` magic number and the file size) file_size: u32, } @@ -92,22 +124,30 @@ impl WaveChunk for RIFFHeader { } } +/// Builds wave data from a given [`WaveFormat`] and raw ADPCM data. +/// +/// # Errors +/// +/// This function returns a [`TooLargeError`] when the length of `data` is greater than or equal to 232 - 82 +/// +/// [`WaveFormat`]: struct.WaveFormat.html +/// [`TooLargeError`]: enum.Error.html#variant.TooLargeError pub fn build_wav(format: WaveFormat, data: &[u8]) -> Result, Error> { debug!("Building file"); - let length: u32 = data - .len() - .try_into() - .map_err(|_| Error::TooLarge(data.len()))?; + // returning `u32::MAX` will make the next check fail + let length: u32 = data.len().try_into().unwrap_or(u32::MAX); let riff_header = RIFFHeader { - file_size: 82 + length, + file_size: length + .checked_add(82) + .ok_or_else(|| Error::TooLargeError(data.len()))?, }; let fact = WaveFact { - length_samples: ((length / u32::from(format.n_block_align)) - * u32::from((format.n_block_align - (7 * format.n_channels)) * 8) + length_samples: ((length / u32::from(format.block_align)) + * u32::from((format.block_align - (7 * format.channels)) * 8) / 4) - / u32::from(format.n_channels), + / u32::from(format.channels), }; let mut buf = Cursor::new(Vec::new()); @@ -125,3 +165,70 @@ pub fn build_wav(format: WaveFormat, data: &[u8]) -> Result, Error> { Ok(buf.into_inner()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_riff_header_to_chunk() { + assert_eq!( + RIFFHeader { file_size: 12345 }.to_chunk(), + b"RIFF\x39\x30\x00\x00WAVE" + ); + } + + #[test] + fn test_wave_fact_to_chunk() { + assert_eq!( + WaveFact { + length_samples: 12345 + } + .to_chunk(), + b"fact\x04\x00\x00\x00\x39\x30\x00\x00" + ); + } + + #[test] + fn test_wave_format_to_chunk() { + assert_eq!( + WaveFormat { + channels: 2, + sample_rate: 44100, + block_align: 140, + } + .to_chunk(), + vec![ + 0x66, 0x6d, 0x74, 0x20, 0x32, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x44, 0xac, + 0x00, 0x00, 0x20, 0xbc, 0x00, 0x00, 0x8c, 0x00, 0x04, 0x00, 0x20, 0x00, 0x80, 0x00, + 0x07, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, + 0xc0, 0x00, 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, + 0x18, 0xff + ] + ); + } + + #[test] + fn test_build_wav() { + let built_wav = build_wav( + WaveFormat { + channels: 2, + sample_rate: 44100, + block_align: 140, + }, + b"data", + ); + assert_eq!( + built_wav.unwrap(), + vec![ + 0x52, 0x49, 0x46, 0x46, 0x56, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45, 0x66, 0x6d, + 0x74, 0x20, 0x32, 0x00, 0x00, 0x00, 0x02, 0x00, 0x02, 0x00, 0x44, 0xac, 0x00, 0x00, + 0x20, 0xbc, 0x00, 0x00, 0x8c, 0x00, 0x04, 0x00, 0x20, 0x00, 0x80, 0x00, 0x07, 0x00, + 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xc0, 0x00, + 0x40, 0x00, 0xf0, 0x00, 0x00, 0x00, 0xcc, 0x01, 0x30, 0xff, 0x88, 0x01, 0x18, 0xff, + 0x66, 0x61, 0x63, 0x74, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x64, 0x61, + 0x74, 0x61, 0x04, 0x00, 0x00, 0x00, 0x64, 0x61, 0x74, 0x61 + ] + ); + } +} diff --git a/src/xact3/xwb.rs b/src/xact3/xwb.rs index 4e1e443..e98dc96 100644 --- a/src/xact3/xwb.rs +++ b/src/xact3/xwb.rs @@ -65,18 +65,12 @@ impl TryInto for Format { return Err(Error::UnsupportedFormat(self.tag)); } - let n_block_align = (u16::from(self.alignment) + 22) * self.channels; - let n_samples_per_block = - (((n_block_align - (7 * self.channels)) * 8) / (4 * self.channels)) + 2; - let n_avg_bytes_per_sec = - (self.sample_rate / u32::from(n_samples_per_block)) * u32::from(n_block_align); + let block_align = (u16::from(self.alignment) + 22) * self.channels; Ok(adpcm::WaveFormat { - n_channels: self.channels, - n_samples_per_sec: self.sample_rate, - n_avg_bytes_per_sec, - n_block_align, - n_samples_per_block, + channels: self.channels, + sample_rate: self.sample_rate, + block_align, }) } }