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, }) } }