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