mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 02:26:46 -06:00
526 lines
21 KiB
Rust
526 lines
21 KiB
Rust
//! Native WAV / BWF / RF64 reader and writer (RIFF chunk layout per the Microsoft multimedia
|
|
//! specification, `bext` per EBU Tech 3285, `ds64` per EBU Tech 3306).
|
|
|
|
use crate::pcm::{PcmKind, Quantizer, deinterleave, le_u16, le_u32, le_u64, sample_format, tag, validate_buffer, validate_layout, write_interleaved};
|
|
use crate::{AudioBuffer, AudioError, AudioInfo, BitDepth, BwfInfo, FileFormat, Result, SampleFormat};
|
|
use std::io::{Cursor, Seek, SeekFrom, Write};
|
|
|
|
const WAVE_FORMAT_PCM: u16 = 1;
|
|
const WAVE_FORMAT_IEEE_FLOAT: u16 = 3;
|
|
const WAVE_FORMAT_EXTENSIBLE: u16 = 0xFFFE;
|
|
/// Tail of the KSDATAFORMAT_SUBTYPE_* GUIDs (bytes 2..16); bytes 0..2 hold the format tag.
|
|
const GUID_TAIL: [u8; 14] = [0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71];
|
|
const BEXT_MIN_LEN: usize = 602;
|
|
|
|
/// Parsed WAV layout (no sample data decoded yet).
|
|
pub(crate) struct WavLayout {
|
|
pub info: AudioInfo,
|
|
pub kind: PcmKind,
|
|
pub data_start: usize,
|
|
pub data_len: usize,
|
|
}
|
|
|
|
/// Outcome of parsing a WAV header.
|
|
pub(crate) enum WavParse {
|
|
/// A PCM / float layout we decode natively.
|
|
Native(WavLayout),
|
|
/// A valid WAV whose codec we hand to symphonia (ADPCM, A-law, ...).
|
|
Foreign { bwf: Option<BwfInfo> },
|
|
}
|
|
|
|
struct Fmt {
|
|
tag: u16,
|
|
channels: u16,
|
|
sample_rate: u32,
|
|
block_align: u16,
|
|
bits: u16,
|
|
valid_bits: u16,
|
|
}
|
|
|
|
fn parse_fmt(c: &[u8]) -> Result<Fmt> {
|
|
let short = || AudioError::Malformed("fmt chunk too short".into());
|
|
let mut fmt = Fmt {
|
|
tag: le_u16(c, 0).ok_or_else(short)?,
|
|
channels: le_u16(c, 2).ok_or_else(short)?,
|
|
sample_rate: le_u32(c, 4).ok_or_else(short)?,
|
|
block_align: le_u16(c, 12).ok_or_else(short)?,
|
|
bits: le_u16(c, 14).unwrap_or(0),
|
|
valid_bits: 0,
|
|
};
|
|
fmt.valid_bits = fmt.bits;
|
|
if fmt.tag == WAVE_FORMAT_EXTENSIBLE {
|
|
// cbSize(2) validBits(2) channelMask(4) subFormat GUID(16)
|
|
if let (Some(valid), Some(sub)) = (le_u16(c, 18), le_u16(c, 24)) {
|
|
if valid != 0 {
|
|
fmt.valid_bits = valid;
|
|
}
|
|
fmt.tag = sub;
|
|
} else {
|
|
return Err(AudioError::Malformed("WAVE_FORMAT_EXTENSIBLE fmt chunk too short".into()));
|
|
}
|
|
}
|
|
Ok(fmt)
|
|
}
|
|
|
|
/// Read a NUL-padded ASCII field.
|
|
fn fixed_str(b: &[u8]) -> String {
|
|
let end = b.iter().position(|&c| c == 0).unwrap_or(b.len());
|
|
String::from_utf8_lossy(b.get(..end).unwrap_or_default()).trim_end().to_string()
|
|
}
|
|
|
|
fn parse_bext(c: &[u8]) -> Option<BwfInfo> {
|
|
if c.len() < 346 {
|
|
return None;
|
|
}
|
|
let lo = u64::from(le_u32(c, 338)?);
|
|
let hi = u64::from(le_u32(c, 342)?);
|
|
Some(BwfInfo {
|
|
description: fixed_str(c.get(0..256)?),
|
|
originator: fixed_str(c.get(256..288)?),
|
|
originator_reference: fixed_str(c.get(288..320)?),
|
|
origination_date: fixed_str(c.get(320..330)?),
|
|
origination_time: fixed_str(c.get(330..338)?),
|
|
time_reference: (hi << 32) | lo,
|
|
})
|
|
}
|
|
|
|
/// Refuse the WAVE file at `at`, which symphonia reads (A-law, ADPCM, ...), if an INFO entry
|
|
/// claims more bytes than the file holds: symphonia allocates each entry's length. Chunks are
|
|
/// walked where symphonia reads them: after the bytes a chunk's parser used, not its length.
|
|
pub(crate) fn check_info(b: &[u8], at: usize) -> Result<()> {
|
|
if tag(b, at + 8) != Some(*b"WAVE") {
|
|
return Ok(());
|
|
}
|
|
let mut pos = at + 12;
|
|
while let Some(id) = tag(b, pos) {
|
|
let Some(len) = le_u32(b, pos + 4).map(|n| n as usize) else { break };
|
|
let (body, pad) = (pos + 8, len & 1);
|
|
let used = match &id {
|
|
b"data" => break,
|
|
b"fmt " => fmt_used(b, body, len),
|
|
b"LIST" if len < 4 => break,
|
|
b"LIST" => {
|
|
let entries = if tag(b, body) == Some(*b"INFO") { info_entries(b, body + 4, len - 4)? } else { 0 };
|
|
// The list pads itself, then the chunk is padded again.
|
|
4 + entries.max(len - 4) + pad
|
|
}
|
|
_ => len,
|
|
};
|
|
pos = body.saturating_add(used).saturating_add(pad);
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// The bytes symphonia reads of a `fmt ` chunk: some formats' parsers go by their own extra size.
|
|
fn fmt_used(b: &[u8], body: usize, len: usize) -> usize {
|
|
match le_u16(b, body) {
|
|
Some(2 | 6 | 7 | 0x11) => le_u16(b, body + 16).map_or(len, |extra| 18 + usize::from(extra)),
|
|
Some(0xfffe) => 40,
|
|
_ => len,
|
|
}
|
|
}
|
|
|
|
/// Check the INFO entries in the `len` bytes from `start`; returns the bytes symphonia reads of
|
|
/// them, with the pad it reads after an odd entry.
|
|
fn info_entries(b: &[u8], start: usize, len: usize) -> Result<usize> {
|
|
let mut used = 0;
|
|
while used + 8 <= len {
|
|
let Some(n) = le_u32(b, start + used + 4).map(|n| n as usize) else { break };
|
|
if n > b.len().saturating_sub(start + used + 8) {
|
|
return Err(AudioError::Malformed("a WAV INFO entry is larger than the file".into()));
|
|
}
|
|
used += 8 + n;
|
|
used += used & 1;
|
|
}
|
|
Ok(used)
|
|
}
|
|
|
|
/// True when the bytes start like a RIFF/RF64/BW64 WAVE file.
|
|
pub(crate) fn is_wav(b: &[u8]) -> bool {
|
|
matches!(tag(b, 0), Some(t) if &t == b"RIFF" || &t == b"RF64" || &t == b"BW64") && tag(b, 8).is_some_and(|t| &t == b"WAVE")
|
|
}
|
|
|
|
/// True when everything from `from` to the end of the file is whole RIFF chunks (four printable
|
|
/// ASCII id bytes, a size that fits, the pad byte after an odd size; the last chunk's pad may be
|
|
/// missing): the metadata of a finished file, not samples.
|
|
fn only_chunks_follow(b: &[u8], from: usize) -> bool {
|
|
let mut pos = from;
|
|
while pos < b.len() {
|
|
let (Some(id), Some(size)) = (tag(b, pos), le_u32(b, pos + 4)) else { return false };
|
|
if !id.iter().all(|c| (0x20..0x7f).contains(c)) {
|
|
return false;
|
|
}
|
|
let size = size as usize;
|
|
let Some(end) = pos.checked_add(8).and_then(|body| body.checked_add(size)) else { return false };
|
|
if end > b.len() {
|
|
return false;
|
|
}
|
|
let next = end + (size & 1);
|
|
if end == b.len() || next == b.len() {
|
|
return true;
|
|
}
|
|
pos = next;
|
|
}
|
|
false
|
|
}
|
|
|
|
pub(crate) fn parse(b: &[u8]) -> Result<WavParse> {
|
|
if !is_wav(b) {
|
|
return Err(AudioError::Malformed("not a RIFF/RF64 WAVE file".into()));
|
|
}
|
|
let is_rf64 = tag(b, 0).is_some_and(|t| &t != b"RIFF");
|
|
let mut ds64_data: Option<u64> = None;
|
|
let mut fmt: Option<Fmt> = None;
|
|
let mut bwf: Option<BwfInfo> = None;
|
|
let mut data: Option<(usize, usize)> = None;
|
|
|
|
let mut pos: usize = 12;
|
|
// Bound the number of chunks we look at so a file of tiny chunks can't spin forever.
|
|
let mut chunks = 0usize;
|
|
while let (Some(id), Some(size32)) = (tag(b, pos), le_u32(b, pos + 4)) {
|
|
chunks += 1;
|
|
if chunks > 100_000 {
|
|
break;
|
|
}
|
|
let body = pos.checked_add(8).ok_or_else(|| AudioError::Malformed("chunk offset overflow".into()))?;
|
|
let remaining = b.len().saturating_sub(body);
|
|
let mut size = u64::from(size32);
|
|
if &id == b"data" && is_rf64 && size32 == u32::MAX {
|
|
size = ds64_data.unwrap_or(remaining as u64);
|
|
}
|
|
let declared = usize::try_from(size).unwrap_or(usize::MAX);
|
|
if &id == b"data" {
|
|
// Lenient: a truncated recording (or a streaming writer's 0 / 0xFFFFFFFF size) still
|
|
// yields whatever audio is actually present.
|
|
// A size of 0 in a file whose RIFF size is right and that has only chunks after it is an
|
|
// empty data chunk followed by metadata (LIST, bext, …); otherwise it is a recording
|
|
// that was never finalised, and what follows is audio.
|
|
let finished = u64::from(le_u32(b, 4).unwrap_or(0)) + 8 == b.len() as u64;
|
|
let empty = size32 == 0 && !is_rf64 && finished && only_chunks_follow(b, body);
|
|
let len = if size32 == 0 && !is_rf64 && !empty { remaining } else { declared.min(remaining) };
|
|
if data.is_none() {
|
|
data = Some((body, len));
|
|
}
|
|
if declared > remaining {
|
|
break;
|
|
}
|
|
} else {
|
|
if declared > remaining {
|
|
// A truncated non-audio chunk at the end of the file: stop scanning.
|
|
break;
|
|
}
|
|
let c = b.get(body..body + declared).unwrap_or_default();
|
|
match &id {
|
|
b"fmt " => fmt = Some(parse_fmt(c)?),
|
|
b"bext" => bwf = parse_bext(c),
|
|
b"ds64" => ds64_data = le_u64(c, 8),
|
|
_ => {}
|
|
}
|
|
}
|
|
let padded = declared.saturating_add(declared & 1);
|
|
pos = match body.checked_add(padded) {
|
|
Some(p) => p,
|
|
None => break,
|
|
};
|
|
}
|
|
|
|
let fmt = fmt.ok_or_else(|| AudioError::Malformed("WAV has no fmt chunk".into()))?;
|
|
if fmt.channels == 0 {
|
|
return Err(AudioError::Malformed("WAV declares zero channels".into()));
|
|
}
|
|
if fmt.sample_rate == 0 {
|
|
return Err(AudioError::Malformed("WAV declares a zero sample rate".into()));
|
|
}
|
|
let channels = usize::from(fmt.channels);
|
|
let (kind, sample_format) = match (fmt.tag, fmt.bits) {
|
|
(WAVE_FORMAT_PCM, bits @ 1..=32) => {
|
|
let bytes = usize::from(bits.div_ceil(8));
|
|
let sf = match fmt.valid_bits.min(bits) {
|
|
0..=8 => SampleFormat::Int8,
|
|
9..=16 => SampleFormat::Int16,
|
|
17..=24 => SampleFormat::Int24,
|
|
_ => SampleFormat::Int32,
|
|
};
|
|
(PcmKind::Int { bytes, big_endian: false, unsigned: bytes == 1 }, sf)
|
|
}
|
|
(WAVE_FORMAT_IEEE_FLOAT, 32) => (PcmKind::Float { bytes: 4, big_endian: false }, SampleFormat::Float32),
|
|
(WAVE_FORMAT_IEEE_FLOAT, 64) => (PcmKind::Float { bytes: 8, big_endian: false }, SampleFormat::Float64),
|
|
(WAVE_FORMAT_PCM | WAVE_FORMAT_IEEE_FLOAT, bits) => {
|
|
return Err(AudioError::Unsupported(format!("WAV format {} with {bits} bits per sample", fmt.tag)));
|
|
}
|
|
_ => return Ok(WavParse::Foreign { bwf }),
|
|
};
|
|
let block = kind.bytes().checked_mul(channels).ok_or_else(|| AudioError::Malformed("block size overflow".into()))?;
|
|
if usize::from(fmt.block_align) != block {
|
|
log::debug!("WAV block_align {} disagrees with computed {block}; using computed", fmt.block_align);
|
|
}
|
|
let (data_start, data_len) = data.ok_or_else(|| AudioError::Malformed("WAV has no data chunk".into()))?;
|
|
let frames = (data_len / block) as u64;
|
|
Ok(WavParse::Native(WavLayout {
|
|
info: AudioInfo { format: FileFormat::Wav, sample_format, sample_rate: fmt.sample_rate, channels: fmt.channels, frames, bwf },
|
|
kind,
|
|
data_start,
|
|
data_len,
|
|
}))
|
|
}
|
|
|
|
pub(crate) fn decode(b: &[u8], layout: &WavLayout) -> Result<AudioBuffer> {
|
|
let end = layout.data_start.saturating_add(layout.data_len).min(b.len());
|
|
let data = b.get(layout.data_start..end).unwrap_or_default();
|
|
let channels = deinterleave(data, layout.kind, usize::from(layout.info.channels), layout.info.frames)?;
|
|
Ok(AudioBuffer { sample_rate: layout.info.sample_rate, channels })
|
|
}
|
|
|
|
/// Default speaker mask for a channel count (callers that know the layout pass their own):
|
|
/// mono, stereo, LCR, quad, 5.0, 5.1, 6.1, 7.1 (sides + rears), 7.1.4.
|
|
fn channel_mask(channels: usize) -> u32 {
|
|
match channels {
|
|
1 => 0x4,
|
|
2 => 0x3,
|
|
3 => 0x7,
|
|
4 => 0x33,
|
|
5 => 0x37,
|
|
6 => 0x3F,
|
|
7 => 0x13F,
|
|
8 => 0x63F,
|
|
// L R C LFE Lrs Rrs Lss Rss Ltf Rtf Ltr Rtr (7.1.4).
|
|
12 => 0x2_D63F,
|
|
n if n <= 18 => (1u32 << n) - 1,
|
|
_ => 0,
|
|
}
|
|
}
|
|
|
|
/// The `dwChannelMask` of a WAVE_FORMAT_EXTENSIBLE file (None for plain PCM/float headers).
|
|
pub(crate) fn read_channel_mask(b: &[u8]) -> Option<u32> {
|
|
if !is_wav(b) {
|
|
return None;
|
|
}
|
|
let mut pos = 12usize;
|
|
for _ in 0..10_000 {
|
|
let (id, size) = (tag(b, pos)?, usize::try_from(le_u32(b, pos + 4)?).ok()?);
|
|
if &id == b"fmt " {
|
|
return (le_u16(b, pos + 8)? == WAVE_FORMAT_EXTENSIBLE).then(|| le_u32(b, pos + 8 + 20)).flatten();
|
|
}
|
|
pos = pos.checked_add(8)?.checked_add(size)?.checked_add(size & 1)?;
|
|
}
|
|
None
|
|
}
|
|
|
|
fn put_fixed_str(out: &mut Vec<u8>, s: &str, len: usize) {
|
|
let bytes = s.as_bytes();
|
|
let n = bytes.len().min(len);
|
|
out.extend_from_slice(bytes.get(..n).unwrap_or_default());
|
|
out.resize(out.len() + (len - n), 0);
|
|
}
|
|
|
|
fn bext_chunk(bwf: &BwfInfo) -> Vec<u8> {
|
|
let mut c = Vec::with_capacity(BEXT_MIN_LEN);
|
|
put_fixed_str(&mut c, &bwf.description, 256);
|
|
put_fixed_str(&mut c, &bwf.originator, 32);
|
|
put_fixed_str(&mut c, &bwf.originator_reference, 32);
|
|
put_fixed_str(&mut c, &bwf.origination_date, 10);
|
|
put_fixed_str(&mut c, &bwf.origination_time, 8);
|
|
c.extend_from_slice(&((bwf.time_reference & 0xFFFF_FFFF) as u32).to_le_bytes());
|
|
c.extend_from_slice(&((bwf.time_reference >> 32) as u32).to_le_bytes());
|
|
c.extend_from_slice(&1u16.to_le_bytes()); // version 1
|
|
c.resize(BEXT_MIN_LEN, 0); // UMID, loudness, reserved; no coding history
|
|
c
|
|
}
|
|
|
|
fn push_chunk(out: &mut Vec<u8>, id: &[u8; 4], body: &[u8]) {
|
|
out.extend_from_slice(id);
|
|
out.extend_from_slice(&u32::try_from(body.len()).unwrap_or(u32::MAX).to_le_bytes());
|
|
out.extend_from_slice(body);
|
|
if body.len() % 2 == 1 {
|
|
out.push(0);
|
|
}
|
|
}
|
|
|
|
/// A WAV written block by block: a placeholder header first, then the real one (the same length,
|
|
/// RF64 above 4 GiB, or always when `force_rf64`) written over it by [`Writer::finish`].
|
|
pub(crate) struct Writer<W> {
|
|
out: W,
|
|
start: u64,
|
|
channels: usize,
|
|
sample_rate: u32,
|
|
format: (u16, bool),
|
|
bwf: Option<BwfInfo>,
|
|
mask: Option<u32>,
|
|
force_rf64: bool,
|
|
quantizers: Vec<Quantizer>,
|
|
frames: u64,
|
|
bytes: Vec<u8>,
|
|
/// Set when a block write has failed: the sink may hold a prefix of that block while
|
|
/// `frames` does not count it, so the writer refuses every later call.
|
|
failed: bool,
|
|
}
|
|
|
|
impl<W: Write + Seek> Writer<W> {
|
|
pub(crate) fn new(
|
|
mut out: W,
|
|
channels: usize,
|
|
sample_rate: u32,
|
|
depth: BitDepth,
|
|
dither: bool,
|
|
bwf: Option<&BwfInfo>,
|
|
mask: Option<u32>,
|
|
force_rf64: bool,
|
|
) -> Result<Self> {
|
|
validate_layout(channels, sample_rate)?;
|
|
let format = sample_format(depth);
|
|
let start = out.stream_position()?;
|
|
out.write_all(&header(channels, sample_rate, format, bwf, mask, 0, force_rf64)?)?;
|
|
let quantizers = Quantizer::per_channel(u32::from(format.0), dither, channels);
|
|
Ok(Writer {
|
|
out,
|
|
start,
|
|
channels,
|
|
sample_rate,
|
|
format,
|
|
bwf: bwf.cloned(),
|
|
mask,
|
|
force_rf64,
|
|
quantizers,
|
|
frames: 0,
|
|
bytes: Vec::new(),
|
|
failed: false,
|
|
})
|
|
}
|
|
|
|
fn header(&self, frames: u64) -> Result<Vec<u8>> {
|
|
header(self.channels, self.sample_rate, self.format, self.bwf.as_ref(), self.mask, frames, self.force_rf64)
|
|
}
|
|
|
|
/// The frame count after `frames` more (RF64 has room for any bounce).
|
|
pub(crate) fn check_room(&self, frames: u64) -> Result<u64> {
|
|
let total = self.frames.saturating_add(frames);
|
|
self.header(total)?;
|
|
Ok(total)
|
|
}
|
|
|
|
/// Appends `frames` frames of planar audio (missing samples are silence).
|
|
pub(crate) fn write(&mut self, block: &[Vec<f32>], frames: usize) -> Result<()> {
|
|
if self.failed {
|
|
return Err(crate::failed_write_error());
|
|
}
|
|
let total = self.check_room(frames as u64)?;
|
|
if let Err(e) = write_interleaved(&mut self.out, &mut self.bytes, block, frames, &mut self.quantizers, self.format, false) {
|
|
self.failed = true;
|
|
return Err(AudioError::from(e));
|
|
}
|
|
self.frames = total;
|
|
Ok(())
|
|
}
|
|
|
|
pub(crate) fn finish(mut self) -> Result<W> {
|
|
if self.failed {
|
|
return Err(crate::failed_write_error());
|
|
}
|
|
let data_len = u64::from(self.format.0 / 8).saturating_mul(self.channels as u64).saturating_mul(self.frames);
|
|
if data_len % 2 == 1 {
|
|
self.out.write_all(&[0])?;
|
|
}
|
|
let head = self.header(self.frames)?;
|
|
self.out.seek(SeekFrom::Start(self.start))?;
|
|
self.out.write_all(&head)?;
|
|
self.out.seek(SeekFrom::End(0))?;
|
|
self.out.flush()?;
|
|
Ok(self.out)
|
|
}
|
|
}
|
|
|
|
/// Encode a WAV file. `force_rf64` writes the RF64 layout even for small files (for tests);
|
|
/// it is used automatically when the file would exceed 4 GiB.
|
|
/// `mask` overrides the speaker mask derived from the channel count.
|
|
pub(crate) fn encode(
|
|
buf: &AudioBuffer,
|
|
depth: BitDepth,
|
|
dither: bool,
|
|
bwf: Option<&BwfInfo>,
|
|
force_rf64: bool,
|
|
mask: Option<u32>,
|
|
) -> Result<Vec<u8>> {
|
|
validate_buffer(buf)?;
|
|
let channels = buf.channels.len();
|
|
let frames = buf.frames();
|
|
let format = sample_format(depth);
|
|
let head = header(channels, buf.sample_rate, format, bwf, mask, frames as u64, force_rf64)?;
|
|
let data_len = (usize::from(format.0 / 8) * channels).checked_mul(frames).ok_or_else(|| AudioError::TooLarge("data size overflow".into()))?;
|
|
let mut out = Vec::new();
|
|
out.try_reserve_exact(head.len().saturating_add(data_len).saturating_add(1))
|
|
.map_err(|_| AudioError::TooLarge(format!("cannot allocate {data_len} bytes for WAV output")))?;
|
|
let mut w = Writer::new(Cursor::new(out), channels, buf.sample_rate, depth, dither, bwf, mask, force_rf64)?;
|
|
w.write(&buf.channels, frames)?;
|
|
Ok(w.finish()?.into_inner())
|
|
}
|
|
|
|
/// Everything before the sample data of a WAV holding `frames` frames.
|
|
pub(crate) fn header(
|
|
channels: usize,
|
|
sample_rate: u32,
|
|
(bits, is_float): (u16, bool),
|
|
bwf: Option<&BwfInfo>,
|
|
mask: Option<u32>,
|
|
frames: u64,
|
|
force_rf64: bool,
|
|
) -> Result<Vec<u8>> {
|
|
let block = usize::from(bits / 8).checked_mul(channels).ok_or_else(|| AudioError::TooLarge("block size overflow".into()))?;
|
|
let block_align = u16::try_from(block).map_err(|_| AudioError::Encode("too many channels for WAV".into()))?;
|
|
let data_len = u64::from(block_align).checked_mul(frames).ok_or_else(|| AudioError::TooLarge("data size overflow".into()))?;
|
|
let extensible = channels > 2 || bits > 16 || mask.is_some_and(|m| m != channel_mask(channels));
|
|
let fmt_tag = if is_float { WAVE_FORMAT_IEEE_FLOAT } else { WAVE_FORMAT_PCM };
|
|
|
|
let mut fmt = Vec::with_capacity(40);
|
|
fmt.extend_from_slice(&(if extensible { WAVE_FORMAT_EXTENSIBLE } else { fmt_tag }).to_le_bytes());
|
|
fmt.extend_from_slice(&(channels as u16).to_le_bytes());
|
|
fmt.extend_from_slice(&sample_rate.to_le_bytes());
|
|
fmt.extend_from_slice(&sample_rate.saturating_mul(u32::from(block_align)).to_le_bytes());
|
|
fmt.extend_from_slice(&block_align.to_le_bytes());
|
|
fmt.extend_from_slice(&bits.to_le_bytes());
|
|
if extensible {
|
|
fmt.extend_from_slice(&22u16.to_le_bytes());
|
|
fmt.extend_from_slice(&bits.to_le_bytes());
|
|
fmt.extend_from_slice(&mask.unwrap_or_else(|| channel_mask(channels)).to_le_bytes());
|
|
fmt.extend_from_slice(&fmt_tag.to_le_bytes());
|
|
fmt.extend_from_slice(&GUID_TAIL);
|
|
}
|
|
|
|
let mut head = Vec::with_capacity(1024);
|
|
let rf64_body_len = 28usize;
|
|
// Reserve room for ds64 (as JUNK when not needed, so a later writer could upgrade in place).
|
|
let mut meta = Vec::new();
|
|
if let Some(bwf) = bwf {
|
|
push_chunk(&mut meta, b"bext", &bext_chunk(bwf));
|
|
}
|
|
push_chunk(&mut meta, b"fmt ", &fmt);
|
|
if is_float {
|
|
push_chunk(&mut meta, b"fact", &u32::try_from(frames).unwrap_or(u32::MAX).to_le_bytes());
|
|
}
|
|
let total = ((4 + (8 + rf64_body_len) + meta.len() + 8) as u64)
|
|
.checked_add(data_len)
|
|
.and_then(|t| t.checked_add(data_len & 1))
|
|
.ok_or_else(|| AudioError::TooLarge("WAV size overflow".into()))?;
|
|
let rf64 = force_rf64 || u32::try_from(total).is_err();
|
|
|
|
if rf64 {
|
|
head.extend_from_slice(b"RF64");
|
|
head.extend_from_slice(&u32::MAX.to_le_bytes());
|
|
head.extend_from_slice(b"WAVE");
|
|
let mut ds64 = Vec::with_capacity(rf64_body_len);
|
|
ds64.extend_from_slice(&total.to_le_bytes());
|
|
ds64.extend_from_slice(&data_len.to_le_bytes());
|
|
ds64.extend_from_slice(&frames.to_le_bytes());
|
|
ds64.extend_from_slice(&0u32.to_le_bytes());
|
|
push_chunk(&mut head, b"ds64", &ds64);
|
|
} else {
|
|
head.extend_from_slice(b"RIFF");
|
|
head.extend_from_slice(&(total as u32).to_le_bytes());
|
|
head.extend_from_slice(b"WAVE");
|
|
push_chunk(&mut head, b"JUNK", &[0u8; 28]);
|
|
}
|
|
head.extend_from_slice(&meta);
|
|
head.extend_from_slice(b"data");
|
|
head.extend_from_slice(&(if rf64 { u32::MAX } else { data_len as u32 }).to_le_bytes());
|
|
Ok(head)
|
|
}
|