use crate::channel_layout::{ChannelLayout, ChannelMask, PanInfo};
use hound::{SampleFormat, WavReader, WavSpec, WavWriter};
use std::io::{BufReader, BufWriter, Read, Seek, Write};
const BYTES_PER_MIB: u64 = 1024 * 1024;
const MIN_MEMORY_ESTIMATE_BYTES: u64 = BYTES_PER_MIB;
#[inline]
pub fn sanitize_sample(sample: f64) -> f64 {
if sample.is_finite() {
sample.clamp(-1.0, 1.0)
} else {
0.0
}
}
#[derive(Clone, Debug)]
pub struct Audio {
pub sample_rate: u32,
pub channels: Vec<Vec<f64>>,
pub bits_per_sample: u16,
pub sample_format: SampleFormat,
pub channel_mask: Option<ChannelMask>,
}
impl Audio {
pub fn channels(&self) -> usize {
self.channels.len()
}
pub fn frames(&self) -> usize {
self.channels.first().map(|c| c.len()).unwrap_or(0)
}
pub fn sanitize_samples(&mut self) -> usize {
let mut changed = 0;
for channel in &mut self.channels {
for sample in channel {
let sanitized = sanitize_sample(*sample);
if *sample != sanitized || !sample.is_finite() {
changed += 1;
}
*sample = sanitized;
}
}
changed
}
pub fn channel_layout(&self) -> ChannelLayout {
self.channel_mask
.filter(|mask| mask.channels() == self.channels())
.map(ChannelLayout::from_channel_mask)
.unwrap_or_else(|| ChannelLayout::from_channel_count(self.channels()))
}
pub fn effective_channel_mask(&self) -> Option<ChannelMask> {
match self.channel_mask {
Some(mask) if mask.bits() == 0 || mask.channels() == self.channels() => Some(mask),
Some(_) => None,
None => self.channel_layout().mask(),
}
}
pub fn pan_info(&self) -> Option<Vec<PanInfo>> {
self.effective_channel_mask().map(ChannelMask::pan)
}
fn wav_spec(&self) -> WavSpec {
WavSpec {
channels: self.channels() as u16,
sample_rate: self.sample_rate,
bits_per_sample: self.bits_per_sample,
sample_format: self.sample_format,
}
}
}
pub fn estimate_audio_memory_bytes(audio: &Audio) -> u64 {
let samples = audio.channels.iter().fold(0u64, |total, channel| {
total.saturating_add(channel.len() as u64)
});
samples
.saturating_mul(std::mem::size_of::<f64>() as u64)
.saturating_add((audio.channels.len() as u64).saturating_mul(256))
}
pub fn estimate_audio_working_set_bytes(audio: &Audio) -> u64 {
estimate_audio_memory_bytes(audio)
.saturating_mul(3)
.max(MIN_MEMORY_ESTIMATE_BYTES)
}
pub fn estimate_stream_memory_bytes(
channels: usize,
block_frames: usize,
frame_size: usize,
sample_rate: u32,
) -> u64 {
let profile_frames = (sample_rate as u64)
.saturating_mul(3)
.saturating_div(2)
.saturating_add(frame_size as u64);
let per_channel_samples = (frame_size as u64)
.saturating_mul(96)
.saturating_add(profile_frames.saturating_mul(2));
let block_samples = (block_frames as u64)
.saturating_mul(channels as u64)
.saturating_mul(4);
per_channel_samples
.saturating_mul(channels as u64)
.saturating_add(block_samples)
.saturating_mul(std::mem::size_of::<f64>() as u64)
.max(MIN_MEMORY_ESTIMATE_BYTES)
}
pub fn estimate_file_memory_bytes<P: AsRef<std::path::Path>>(path: P) -> Result<u64, String> {
let size = std::fs::metadata(path.as_ref())
.map_err(|error| format!("read input metadata: {error}"))?
.len();
Ok(size.saturating_mul(8).max(MIN_MEMORY_ESTIMATE_BYTES))
}
pub fn ensure_memory_limit(
estimated_bytes: u64,
max_memory_mb: Option<usize>,
context: &str,
) -> Result<(), String> {
let Some(max_memory_mb) = max_memory_mb else {
return Ok(());
};
if max_memory_mb == 0 {
return Err("--max-memory must be at least 1 MiB".into());
}
let limit = (max_memory_mb as u64).saturating_mul(BYTES_PER_MIB);
if estimated_bytes > limit {
let estimated_mib = estimated_bytes
.saturating_add(BYTES_PER_MIB - 1)
.saturating_div(BYTES_PER_MIB);
return Err(format!(
"{context} requires approximately {estimated_mib} MiB, but --max-memory allows {max_memory_mb} MiB; use --stream for WAV or raise the limit"
));
}
Ok(())
}
pub fn read_audio<P: AsRef<std::path::Path>>(path: P) -> Result<Audio, String> {
let path = path.as_ref();
let header = {
use std::io::Read;
let mut file = std::fs::File::open(path).map_err(|e| format!("open: {e}"))?;
let mut header = [0u8; 12];
let n = file.read(&mut header).map_err(|e| format!("read: {e}"))?;
header[..n].to_vec()
};
if crate::decode::AudioFormat::detect(path, &header) == crate::decode::AudioFormat::Wav {
return read_wav(path);
}
let pcm = crate::decode::decode_file(path)?;
Ok(pcm.into_audio())
}
pub fn read_wav<P: AsRef<std::path::Path>>(path: P) -> Result<Audio, String> {
let channel_mask = read_wav_channel_mask(path.as_ref())?;
let reader = WavReader::open(&path).map_err(|e| format!("open: {e}"))?;
read_wav_reader(reader, channel_mask)
}
pub fn read_wav_bytes(bytes: Vec<u8>) -> Result<Audio, String> {
let channel_mask = read_wav_channel_mask_bytes(&bytes)?;
let reader = WavReader::new(std::io::Cursor::new(bytes)).map_err(|e| format!("open: {e}"))?;
read_wav_reader(reader, channel_mask)
}
fn read_wav_reader<R: std::io::Read>(
mut reader: WavReader<R>,
channel_mask: Option<ChannelMask>,
) -> Result<Audio, String> {
let spec = reader.spec();
let nchan = spec.channels as usize;
if nchan == 0 {
return Err("0 channels".into());
}
let max = (1u64 << (spec.bits_per_sample - 1)) as f64; let inv = 1.0 / max;
let mut channels: Vec<Vec<f64>> = (0..nchan).map(|_| Vec::new()).collect();
match spec.sample_format {
SampleFormat::Float => {
let samples: Result<Vec<f32>, String> = reader
.samples::<f32>()
.map(|s| s.map_err(|e| format!("read: {e}")))
.collect();
for (i, v) in samples?.iter().enumerate() {
channels[i % nchan].push(sanitize_sample(*v as f64));
}
}
SampleFormat::Int => {
if spec.bits_per_sample <= 16 {
let samples: Result<Vec<i16>, String> = reader
.samples::<i16>()
.map(|s| s.map_err(|e| format!("read: {e}")))
.collect();
for (i, v) in samples?.iter().enumerate() {
channels[i % nchan].push(sanitize_sample(*v as f64 * inv));
}
} else {
let samples: Result<Vec<i32>, String> = reader
.samples::<i32>()
.map(|s| s.map_err(|e| format!("read: {e}")))
.collect();
for (i, v) in samples?.iter().enumerate() {
channels[i % nchan].push(sanitize_sample(*v as f64 * inv));
}
}
}
}
Ok(Audio {
sample_rate: spec.sample_rate,
channels,
bits_per_sample: spec.bits_per_sample,
sample_format: spec.sample_format,
channel_mask,
})
}
fn read_wav_channel_mask(path: &std::path::Path) -> Result<Option<ChannelMask>, String> {
let mut file =
std::fs::File::open(path).map_err(|error| format!("open WAV header: {error}"))?;
let mut header = [0u8; 12];
if file.read_exact(&mut header).is_err() || &header[8..12] != b"WAVE" {
return Ok(None);
}
loop {
let mut chunk = [0u8; 8];
match file.read_exact(&mut chunk) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::UnexpectedEof => return Ok(None),
Err(error) => return Err(format!("read WAV chunk header: {error}")),
}
let size = u32::from_le_bytes(chunk[4..8].try_into().expect("WAV chunk size")) as usize;
if &chunk[..4] == b"fmt " {
if size < 40 {
return Ok(None);
}
if size > 1 << 20 {
return Err("WAV fmt chunk is too large".into());
}
let mut body = vec![0u8; size];
file.read_exact(&mut body)
.map_err(|error| format!("read WAV fmt chunk: {error}"))?;
return parse_wav_channel_mask_fmt(&body);
}
use std::io::{Seek, SeekFrom};
let skip = size.saturating_add(size & 1);
file.seek(SeekFrom::Current(
i64::try_from(skip).map_err(|_| "WAV chunk is too large to seek".to_string())?,
))
.map_err(|error| format!("skip WAV chunk: {error}"))?;
}
}
fn read_wav_channel_mask_bytes(bytes: &[u8]) -> Result<Option<ChannelMask>, String> {
if bytes.len() < 12 || &bytes[8..12] != b"WAVE" {
return Ok(None);
}
let mut offset = 12usize;
while offset.saturating_add(8) <= bytes.len() {
let id = &bytes[offset..offset + 4];
let size = u32::from_le_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()) as usize;
let body_start = offset + 8;
let body_end = body_start.saturating_add(size);
if body_end > bytes.len() {
break;
}
if id == b"fmt " && size >= 40 {
let body = &bytes[body_start..body_end];
return parse_wav_channel_mask_fmt(body);
}
offset = body_end.saturating_add(size & 1);
}
Ok(None)
}
fn parse_wav_channel_mask_fmt(body: &[u8]) -> Result<Option<ChannelMask>, String> {
if body.len() < 40 {
return Ok(None);
}
let format_tag = u16::from_le_bytes(body[0..2].try_into().expect("WAV format tag"));
if format_tag != 0xfffe {
return Ok(None);
}
let channels = u16::from_le_bytes(body[2..4].try_into().expect("WAV channel count")) as usize;
let mask_bits = u32::from_le_bytes(body[20..24].try_into().expect("WAV channel mask"));
let mask = ChannelMask::from_bits(mask_bits)
.ok_or_else(|| format!("WAV channel mask 0x{mask_bits:08x} is invalid"))?;
if mask.bits() != 0 && mask.channels() != channels {
return Err(format!(
"WAV channel mask has {} positions but fmt declares {channels} channels",
mask.channels()
));
}
Ok(Some(mask))
}
pub fn write_audio<P: AsRef<std::path::Path>>(
path: P,
audio: &Audio,
options: crate::encode::EncodeOptions,
) -> Result<(), String> {
crate::encode::write_audio(path, audio, options)
}
pub fn write_wav<P: AsRef<std::path::Path>>(path: P, audio: &Audio) -> Result<(), String> {
let path = path.as_ref();
if audio.channels() == 0 {
return Err("WAV output requires at least one channel".into());
}
let spec = audio.wav_spec();
let writer = WavWriter::create(path, spec).map_err(|e| format!("create: {e}"))?;
write_wav_writer(writer, audio)?;
patch_wav_channel_mask_file(path, audio)
}
pub fn write_wav_bytes(audio: &Audio) -> Result<Vec<u8>, String> {
if audio.channels() == 0 {
return Err("WAV output requires at least one channel".into());
}
let mut bytes = Vec::new();
{
let cursor = std::io::Cursor::new(&mut bytes);
let writer =
WavWriter::new(cursor, audio.wav_spec()).map_err(|e| format!("create: {e}"))?;
write_wav_writer(writer, audio)?;
}
patch_wav_channel_mask_bytes(&mut bytes, audio)?;
Ok(bytes)
}
fn patch_wav_channel_mask_file(path: &std::path::Path, audio: &Audio) -> Result<(), String> {
write_wav_channel_mask(path, audio.channels(), audio.effective_channel_mask())
}
pub fn write_wav_channel_mask(
path: impl AsRef<std::path::Path>,
channels: usize,
channel_mask: Option<ChannelMask>,
) -> Result<(), String> {
if channels <= 2 {
return Ok(());
}
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(path.as_ref())
.map_err(|error| format!("open WAV header for channel mask: {error}"))?;
use std::io::{Seek, SeekFrom};
let mut header = [0u8; 44];
file.read_exact(&mut header)
.map_err(|error| format!("read WAV header for channel mask: {error}"))?;
if &header[..4] != b"RIFF"
|| &header[8..12] != b"WAVE"
|| &header[12..16] != b"fmt "
|| u32::from_le_bytes(header[16..20].try_into().expect("WAV fmt size")) < 40
|| u16::from_le_bytes(header[20..22].try_into().expect("WAV format tag")) != 0xfffe
{
return Err("multichannel WAV output is not WAVE_FORMAT_EXTENSIBLE".into());
}
file.seek(SeekFrom::Start(40))
.map_err(|error| format!("seek WAV channel mask: {error}"))?;
let bits = channel_mask
.filter(|mask| mask.bits() == 0 || mask.channels() == channels)
.map_or(0, ChannelMask::bits);
file.write_all(&bits.to_le_bytes())
.map_err(|error| format!("write WAV channel mask: {error}"))
}
fn patch_wav_channel_mask_bytes(bytes: &mut [u8], audio: &Audio) -> Result<(), String> {
if audio.channels() <= 2 {
return Ok(());
}
if bytes.len() < 44 || &bytes[12..16] != b"fmt " {
return Err("WAV output has no fmt chunk to store channel mask".into());
}
let fmt_size = u32::from_le_bytes(bytes[16..20].try_into().expect("WAV fmt size"));
if fmt_size < 40
|| u16::from_le_bytes(bytes[20..22].try_into().expect("WAV format tag")) != 0xfffe
{
return Err("multichannel WAV output is not WAVE_FORMAT_EXTENSIBLE".into());
}
let bits = audio
.effective_channel_mask()
.filter(|mask| mask.bits() == 0 || mask.channels() == audio.channels())
.map_or(0, ChannelMask::bits);
bytes[40..44].copy_from_slice(&bits.to_le_bytes());
Ok(())
}
fn write_wav_writer<W: std::io::Write + std::io::Seek>(
mut writer: WavWriter<W>,
audio: &Audio,
) -> Result<(), String> {
let nchan = audio.channels();
let frames = audio.frames();
match audio.sample_format {
SampleFormat::Float => {
for f in 0..frames {
for ch in 0..nchan {
let v = sanitize_sample(audio.channels[ch].get(f).copied().unwrap_or(0.0));
writer
.write_sample(v as f32)
.map_err(|e| format!("write: {e}"))?;
}
}
}
SampleFormat::Int => {
let max = (1i64 << (audio.bits_per_sample - 1)) as f64;
let hi = (max - 1.0) as i64;
let lo = -max as i64;
if audio.bits_per_sample <= 16 {
for f in 0..frames {
for ch in 0..nchan {
let v = sanitize_sample(audio.channels[ch].get(f).copied().unwrap_or(0.0));
let q = ((v * max).round() as i64).min(hi).max(lo);
writer
.write_sample(q as i16)
.map_err(|e| format!("write: {e}"))?;
}
}
} else {
for f in 0..frames {
for ch in 0..nchan {
let v = sanitize_sample(audio.channels[ch].get(f).copied().unwrap_or(0.0));
let q = ((v * max).round() as i64).min(hi).max(lo);
writer
.write_sample(q as i32)
.map_err(|e| format!("write: {e}"))?;
}
}
}
}
}
writer.finalize().map_err(|e| format!("finalize: {e}"))?;
Ok(())
}
pub struct WavStreamReader<R: Read + Seek> {
reader: WavReader<R>,
spec: WavSpec,
channel_mask: Option<ChannelMask>,
}
impl WavStreamReader<BufReader<std::fs::File>> {
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, String> {
let channel_mask = read_wav_channel_mask(path.as_ref())?;
let file = std::fs::File::open(path).map_err(|e| format!("open: {e}"))?;
let reader = WavReader::new(BufReader::new(file)).map_err(|e| format!("open: {e}"))?;
Self::from_reader_with_mask(reader, channel_mask)
}
}
impl<R: Read + Seek> WavStreamReader<R> {
pub fn from_reader(reader: WavReader<R>) -> Result<Self, String> {
Self::from_reader_with_mask(reader, None)
}
fn from_reader_with_mask(
reader: WavReader<R>,
channel_mask: Option<ChannelMask>,
) -> Result<Self, String> {
let spec = reader.spec();
if spec.channels == 0 {
return Err("0 channels".into());
}
if spec.sample_format == SampleFormat::Int && !(1..=32).contains(&spec.bits_per_sample) {
return Err(format!(
"unsupported integer WAV bit depth: {}",
spec.bits_per_sample
));
}
Ok(Self {
reader,
spec,
channel_mask,
})
}
pub fn spec(&self) -> WavSpec {
self.spec
}
pub fn channel_mask(&self) -> Option<ChannelMask> {
self.channel_mask
}
pub fn next_block(&mut self, max_frames: usize) -> Result<Option<Vec<Vec<f64>>>, String> {
if max_frames == 0 {
return Err("stream block size must be at least one frame".into());
}
let nchan = self.spec.channels as usize;
let max_samples = max_frames.saturating_mul(nchan);
let mut interleaved = Vec::with_capacity(max_samples);
match self.spec.sample_format {
SampleFormat::Float => {
for sample in self.reader.samples::<f32>().take(max_samples) {
interleaved.push(sanitize_sample(
sample.map_err(|e| format!("read: {e}"))? as f64
));
}
}
SampleFormat::Int if self.spec.bits_per_sample <= 16 => {
let max = (1u64 << (self.spec.bits_per_sample - 1)) as f64;
let inv = 1.0 / max;
for sample in self.reader.samples::<i16>().take(max_samples) {
interleaved.push(sanitize_sample(
sample.map_err(|e| format!("read: {e}"))? as f64 * inv,
));
}
}
SampleFormat::Int => {
let max = (1u64 << (self.spec.bits_per_sample - 1)) as f64;
let inv = 1.0 / max;
for sample in self.reader.samples::<i32>().take(max_samples) {
interleaved.push(sanitize_sample(
sample.map_err(|e| format!("read: {e}"))? as f64 * inv,
));
}
}
}
if interleaved.is_empty() {
return Ok(None);
}
if interleaved.len() % nchan != 0 {
return Err("truncated WAV frame at end of input".into());
}
let frames = interleaved.len() / nchan;
let mut channels: Vec<Vec<f64>> = (0..nchan).map(|_| Vec::with_capacity(frames)).collect();
for (index, sample) in interleaved.into_iter().enumerate() {
channels[index % nchan].push(sample);
}
Ok(Some(channels))
}
}
pub struct WavStreamWriter<W: Write + Seek> {
writer: WavWriter<W>,
spec: WavSpec,
}
impl WavStreamWriter<BufWriter<std::fs::File>> {
pub fn create<P: AsRef<std::path::Path>>(path: P, spec: WavSpec) -> Result<Self, String> {
let file = std::fs::File::create(path).map_err(|e| format!("create: {e}"))?;
let writer =
WavWriter::new(BufWriter::new(file), spec).map_err(|e| format!("create: {e}"))?;
Self::from_writer(writer, spec)
}
}
impl<W: Write + Seek> WavStreamWriter<W> {
pub fn from_writer(writer: WavWriter<W>, spec: WavSpec) -> Result<Self, String> {
if spec.channels == 0 {
return Err("0 channels".into());
}
if spec.sample_format == SampleFormat::Int && !(1..=32).contains(&spec.bits_per_sample) {
return Err(format!(
"unsupported integer WAV bit depth: {}",
spec.bits_per_sample
));
}
Ok(Self { writer, spec })
}
pub fn write_block(&mut self, channels: &[Vec<f64>]) -> Result<(), String> {
let nchan = self.spec.channels as usize;
if channels.len() != nchan {
return Err(format!("expected {nchan} channels, got {}", channels.len()));
}
let frames = channels.first().map(Vec::len).unwrap_or(0);
if channels.iter().any(|channel| channel.len() != frames) {
return Err("stream blocks must have equal channel lengths".into());
}
match self.spec.sample_format {
SampleFormat::Float => {
for frame in 0..frames {
for channel in channels {
self.writer
.write_sample(sanitize_sample(channel[frame]) as f32)
.map_err(|e| format!("write: {e}"))?;
}
}
}
SampleFormat::Int if self.spec.bits_per_sample <= 16 => {
let max = (1i64 << (self.spec.bits_per_sample - 1)) as f64;
let hi = (max - 1.0) as i64;
let lo = -max as i64;
for frame in 0..frames {
for channel in channels {
let value = sanitize_sample(channel[frame]);
let quantized = ((value * max).round() as i64).min(hi).max(lo);
self.writer
.write_sample(quantized as i16)
.map_err(|e| format!("write: {e}"))?;
}
}
}
SampleFormat::Int => {
let max = (1i64 << (self.spec.bits_per_sample - 1)) as f64;
let hi = (max - 1.0) as i64;
let lo = -max as i64;
for frame in 0..frames {
for channel in channels {
let value = sanitize_sample(channel[frame]);
let quantized = ((value * max).round() as i64).min(hi).max(lo);
self.writer
.write_sample(quantized as i32)
.map_err(|e| format!("write: {e}"))?;
}
}
}
}
Ok(())
}
pub fn finalize(self) -> Result<(), String> {
self.writer.finalize().map_err(|e| format!("finalize: {e}"))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
p.push(format!("denoize_audio_{}_{}", std::process::id(), name));
p
}
#[test]
fn sanitize_samples_maps_nonfinite_and_extreme_values_to_safe_pcm() {
let mut audio = Audio {
sample_rate: 48_000,
channels: vec![vec![
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
2.0,
-2.0,
0.25,
]],
bits_per_sample: 32,
sample_format: SampleFormat::Float,
channel_mask: None,
};
assert_eq!(audio.sanitize_samples(), 5);
assert_eq!(audio.channels[0], vec![0.0, 0.0, 0.0, 1.0, -1.0, 0.25]);
}
#[test]
fn finite_pcm_clipping_is_symmetric_and_idempotent() {
let input = [-1.25, -1.0, -0.5, 0.0, 0.5, 1.0, 1.25];
let clipped: Vec<_> = input.iter().copied().map(sanitize_sample).collect();
assert_eq!(clipped, [-1.0, -1.0, -0.5, 0.0, 0.5, 1.0, 1.0]);
assert_eq!(
clipped,
clipped
.iter()
.copied()
.map(sanitize_sample)
.collect::<Vec<_>>()
);
}
#[test]
fn wav_write_sanitizes_nonfinite_samples_and_supports_empty_frames() {
let path = tmp("nonfinite.wav");
let audio = Audio {
sample_rate: 48_000,
channels: vec![vec![
f64::NAN,
f64::INFINITY,
f64::NEG_INFINITY,
2.0,
-2.0,
0.25,
]],
bits_per_sample: 32,
sample_format: SampleFormat::Float,
channel_mask: None,
};
write_wav(&path, &audio).unwrap();
let decoded = read_wav(&path).unwrap();
assert_eq!(decoded.channels[0][..5], [0.0, 0.0, 0.0, 1.0, -1.0]);
assert!((decoded.channels[0][5] - 0.25).abs() < 1e-6);
std::fs::remove_file(&path).unwrap();
let empty_path = tmp("empty.wav");
let empty = Audio {
sample_rate: 48_000,
channels: vec![Vec::new()],
bits_per_sample: 32,
sample_format: SampleFormat::Float,
channel_mask: None,
};
write_wav(&empty_path, &empty).unwrap();
let decoded_empty = read_wav(&empty_path).unwrap();
assert_eq!(decoded_empty.channels(), 1);
assert_eq!(decoded_empty.frames(), 0);
std::fs::remove_file(empty_path).unwrap();
}
#[test]
fn wav_stream_writer_sanitizes_nonfinite_samples() {
let path = tmp("stream_nonfinite.wav");
let spec = WavSpec {
channels: 1,
sample_rate: 48_000,
bits_per_sample: 32,
sample_format: SampleFormat::Float,
};
let mut writer = WavStreamWriter::create(&path, spec).unwrap();
writer
.write_block(&[vec![f64::NAN, f64::INFINITY, -2.0, 0.5]])
.unwrap();
writer.finalize().unwrap();
let decoded = read_wav(&path).unwrap();
assert_eq!(decoded.channels[0][..3], [0.0, 0.0, -1.0]);
assert!((decoded.channels[0][3] - 0.5).abs() < 1e-6);
std::fs::remove_file(path).unwrap();
}
#[test]
fn wav_16bit_roundtrip() {
let path = tmp("rt16.wav");
let sr = 16000u32;
let spec = WavSpec {
channels: 1,
sample_rate: sr,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut w = WavWriter::create(&path, spec).unwrap();
let mut signal = Vec::new();
for i in 0..sr as usize {
let v = (2.0 * std::f64::consts::PI * 220.0 * i as f64 / sr as f64).sin() * 0.5;
signal.push(v);
w.write_sample((v * 32767.0) as i16).unwrap();
}
w.finalize().unwrap();
let audio = read_wav(&path).unwrap();
assert_eq!(audio.sample_rate, sr);
assert_eq!(audio.channels(), 1);
assert_eq!(audio.frames(), sr as usize);
for (i, &sig) in signal.iter().enumerate() {
assert!((audio.channels[0][i] - sig).abs() < 1e-3, "@{i}");
}
let _ = std::fs::remove_file(&path);
}
#[test]
fn read_audio_preserves_wav_format() {
let path = tmp("preserve16.wav");
let spec = WavSpec {
channels: 1,
sample_rate: 16000,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut w = WavWriter::create(&path, spec).unwrap();
w.write_sample(123i16).unwrap();
w.finalize().unwrap();
let audio = read_audio(&path).unwrap();
assert_eq!(audio.bits_per_sample, 16);
assert_eq!(audio.sample_format, SampleFormat::Int);
let _ = std::fs::remove_file(&path);
}
#[test]
fn wav_stream_reader_and_writer_roundtrip_blocks() {
let input = tmp("stream_in.wav");
let output = tmp("stream_out.wav");
let spec = WavSpec {
channels: 2,
sample_rate: 16_000,
bits_per_sample: 16,
sample_format: SampleFormat::Int,
};
let mut writer = WavWriter::create(&input, spec).unwrap();
for frame in 0..257 {
writer.write_sample((frame as i16).wrapping_mul(3)).unwrap();
writer
.write_sample(-(frame as i16).wrapping_mul(2))
.unwrap();
}
writer.finalize().unwrap();
let mut reader = WavStreamReader::open(&input).unwrap();
assert_eq!(reader.spec(), spec);
let first = reader.next_block(100).unwrap().unwrap();
assert_eq!(
first.iter().map(Vec::len).collect::<Vec<_>>(),
vec![100, 100]
);
let second = reader.next_block(100).unwrap().unwrap();
assert_eq!(
second.iter().map(Vec::len).collect::<Vec<_>>(),
vec![100, 100]
);
let third = reader.next_block(100).unwrap().unwrap();
assert_eq!(third.iter().map(Vec::len).collect::<Vec<_>>(), vec![57, 57]);
assert!(reader.next_block(100).unwrap().is_none());
let mut stream_writer = WavStreamWriter::create(&output, spec).unwrap();
stream_writer.write_block(&first).unwrap();
stream_writer.write_block(&second).unwrap();
stream_writer.write_block(&third).unwrap();
stream_writer.finalize().unwrap();
let roundtrip = read_wav(&output).unwrap();
assert_eq!(roundtrip.frames(), 257);
assert_eq!(roundtrip.channels(), 2);
assert!((roundtrip.channels[0][120] - (360.0 / 32_768.0)).abs() < 1e-5);
assert!((roundtrip.channels[1][120] + (240.0 / 32_768.0)).abs() < 1e-5);
let _ = std::fs::remove_file(input);
let _ = std::fs::remove_file(output);
}
#[test]
fn memory_estimates_scale_with_audio_and_stream_blocks() {
let small = Audio {
sample_rate: 16_000,
channels: vec![vec![0.0; 1_000]],
bits_per_sample: 16,
sample_format: SampleFormat::Int,
channel_mask: None,
};
let large = Audio {
channels: vec![vec![0.0; 2_000], vec![0.0; 2_000]],
..small.clone()
};
assert!(estimate_audio_memory_bytes(&small) > 0);
assert!(estimate_audio_memory_bytes(&large) > estimate_audio_memory_bytes(&small));
assert!(estimate_audio_working_set_bytes(&large) >= estimate_audio_memory_bytes(&large));
assert!(
estimate_stream_memory_bytes(2, 4_096, 2_048, 48_000)
> estimate_stream_memory_bytes(2, 1_024, 2_048, 48_000)
);
}
#[test]
fn reports_standard_surround_layouts_without_mixing_channels() {
let audio = Audio {
sample_rate: 48_000,
channels: vec![vec![0.0; 2]; 6],
bits_per_sample: 32,
sample_format: SampleFormat::Float,
channel_mask: None,
};
assert_eq!(
audio.channel_layout(),
crate::channel_layout::ChannelLayout::FivePointOne
);
assert_eq!(audio.channels(), audio.channel_layout().channels());
}
#[test]
fn multichannel_wav_roundtrip_preserves_explicit_speaker_mask() {
let path = tmp("mask_roundtrip.wav");
let mask = ChannelMask::from_bits(
ChannelMask::FRONT_LEFT
| ChannelMask::FRONT_RIGHT
| ChannelMask::FRONT_CENTER
| ChannelMask::LFE1
| ChannelMask::SIDE_LEFT
| ChannelMask::SIDE_RIGHT,
)
.unwrap();
let audio = Audio {
sample_rate: 48_000,
channels: vec![vec![0.0, 0.1]; 6],
bits_per_sample: 16,
sample_format: SampleFormat::Int,
channel_mask: Some(mask),
};
write_wav(&path, &audio).unwrap();
let decoded = read_wav(&path).unwrap();
assert_eq!(decoded.channel_mask, Some(mask));
assert_eq!(decoded.channel_layout(), ChannelLayout::Unknown(6));
let pan = decoded.pan_info().unwrap();
assert_eq!(pan.len(), 6);
assert_eq!(pan[4].azimuth_degrees, -90.0);
let bytes = write_wav_bytes(&audio).unwrap();
assert_eq!(read_wav_bytes(bytes).unwrap().channel_mask, Some(mask));
let _ = std::fs::remove_file(path);
}
#[test]
fn memory_limit_reports_clear_overflow() {
let error = ensure_memory_limit(2 * 1024 * 1024, Some(1), "decoded audio").unwrap_err();
assert!(error.contains("decoded audio"));
assert!(error.contains("--max-memory allows 1 MiB"));
ensure_memory_limit(1024, Some(1), "decoded audio").unwrap();
ensure_memory_limit(2 * 1024 * 1024, None, "decoded audio").unwrap();
}
}