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.
This commit is contained in:
Simon Bruder 2020-07-05 21:17:42 +02:00
parent 36d3b5b6bf
commit f9c0140100
No known key found for this signature in database
GPG key ID: 6F03E0000CC5B62F
3 changed files with 142 additions and 37 deletions

View file

@ -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;

View file

@ -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 2<sup>32</sup> bytes (2<sup>32</sup> - 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<u8>;
}
/// 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::<LE>(2 + 2 + 4 + 4 + 2 + 2 + 2 + 2 + 2 + 4 * COEFFS.len() as u32)
.unwrap();
buf.write_u16::<LE>(2).unwrap(); // WAVE_FORMAT_ADPCM
buf.write_u16::<LE>(self.n_channels).unwrap();
buf.write_u32::<LE>(self.n_samples_per_sec).unwrap();
buf.write_u32::<LE>(self.n_avg_bytes_per_sec).unwrap();
buf.write_u16::<LE>(self.n_block_align).unwrap();
buf.write_u16::<LE>(self.channels).unwrap();
buf.write_u32::<LE>(self.sample_rate).unwrap();
buf.write_u32::<LE>(self.avg_bytes_per_sec()).unwrap(); // nAvgBytesPerSec
buf.write_u16::<LE>(self.block_align).unwrap();
buf.write_u16::<LE>(4).unwrap(); // wBitsPerSample
buf.write_u16::<LE>(32).unwrap(); // cbSize
buf.write_u16::<LE>(self.n_samples_per_block).unwrap();
buf.write_u16::<LE>(7).unwrap(); // wNumCoeff
buf.write_u16::<LE>(self.samples_per_block()).unwrap();
buf.write_u16::<LE>(COEFFS.len().try_into().unwrap())
.unwrap(); // nNumCoeff
for coef_set in COEFFS {
buf.write_i16::<LE>(coef_set.0).unwrap();
buf.write_i16::<LE>(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<u8> {
let mut buf = Cursor::new(Vec::new());
write!(buf, "fact").unwrap();
buf.write_u32::<LE>(4).unwrap();
buf.write_u32::<LE>(4).unwrap(); // length of fact chunk
buf.write_u32::<LE>(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 2<sup>32</sup> - 82
///
/// [`WaveFormat`]: struct.WaveFormat.html
/// [`TooLargeError`]: enum.Error.html#variant.TooLargeError
pub fn build_wav(format: WaveFormat, data: &[u8]) -> Result<Vec<u8>, 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<Vec<u8>, 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
]
);
}
}

View file

@ -65,18 +65,12 @@ impl TryInto<adpcm::WaveFormat> 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,
})
}
}