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:
parent
36d3b5b6bf
commit
f9c0140100
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Reference in a new issue