pub mod chunks;
pub mod conversion;
pub mod core;
pub mod error;
pub mod header;
pub mod iter;
pub mod wav_type;
use error::FormatError;
use i24::i24;
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::Path;
pub use crate::conversion::{AudioSample, ConvertSlice, ConvertTo};
pub use crate::chunks::{FactChunk, FmtChunk, ListChunk, DATA, FACT, LIST, RIFF, WAVE};
pub use crate::core::{wav_spec, ReadSeek, Samples, Wav};
pub use crate::error::{WaversError, WaversResult};
pub use crate::header::WavHeader;
pub use crate::wav_type::{format_info_to_wav_type, wav_type_to_format_info, FormatCode, WavType};
#[macro_export]
macro_rules! log {
($level:expr, $($arg:tt)+) => {
#[cfg(feature = "logging")]
log::log!($level, $($arg)+);
};
}
#[inline(always)]
pub fn read<T: AudioSample, P: AsRef<Path>>(path: P) -> WaversResult<(Samples<T>, i32)>
where
i16: ConvertTo<T>,
i24: ConvertTo<T>,
i32: ConvertTo<T>,
f32: ConvertTo<T>,
f64: ConvertTo<T>,
Box<[i16]>: ConvertSlice<T>,
Box<[i24]>: ConvertSlice<T>,
Box<[i32]>: ConvertSlice<T>,
Box<[f32]>: ConvertSlice<T>,
Box<[f64]>: ConvertSlice<T>,
{
let mut wav: Wav<T> = Wav::from_path(&path)?;
let samples = wav.read()?;
log!(
log::Level::Debug,
"Read wav file from {}\n{}",
path.as_ref().display(),
wav,
);
Ok((samples, wav.sample_rate()))
}
#[inline(always)]
pub fn write<T: AudioSample, P: AsRef<Path>>(
fp: P,
samples: &[T],
sample_rate: i32,
n_channels: u16,
) -> WaversResult<()>
where
i16: ConvertTo<T>,
i24: ConvertTo<T>,
i32: ConvertTo<T>,
f32: ConvertTo<T>,
f64: ConvertTo<T>,
Box<[i16]>: ConvertSlice<T>,
Box<[i24]>: ConvertSlice<T>,
Box<[i32]>: ConvertSlice<T>,
Box<[f32]>: ConvertSlice<T>,
Box<[f64]>: ConvertSlice<T>,
{
let s = Samples::from(samples);
let samples_bytes = s.as_bytes();
let new_header = WavHeader::new_header::<T>(sample_rate, n_channels, s.len())?;
let f = File::create(&fp)?;
let mut buf_writer = BufWriter::new(f);
match new_header.fmt_chunk.format {
FormatCode::WAV_FORMAT_PCM | FormatCode::WAV_FORMAT_IEEE_FLOAT => {
let header_bytes = new_header.as_base_bytes();
buf_writer.write_all(&header_bytes)?;
}
FormatCode::WAVE_FORMAT_EXTENSIBLE => {
let header_bytes = new_header.as_extended_bytes();
buf_writer.write_all(&header_bytes)?;
}
_ => {
return Err(FormatError::InvalidTypeId("Invalid type ID").into());
}
}
buf_writer.write_all(&DATA)?;
let data_size_bytes = samples_bytes.len() as u32; buf_writer.write_all(&data_size_bytes.to_ne_bytes())?; buf_writer.write_all(&samples_bytes)?; log!(
log::Level::Debug,
"Wrote wav file to {}",
fp.as_ref().display()
);
Ok(())
}
#[cfg(test)]
mod lib_tests {
use approx_eq::assert_approx_eq;
use std::io::BufRead;
use std::{fs::File, path::Path, str::FromStr};
use super::{read, write, Samples, Wav};
const TEST_OUTPUT: &str = "./test_resources/tmp/";
#[test]
fn test_write() {
let expected_path = "./test_resources/one_channel_f32.txt";
let out_path = "./test_resources/tmp/one_channel_f32_tmp.wav";
let mut wav: Wav<f32> = Wav::from_path("./test_resources/one_channel_i16.wav")
.expect("Failed to open file wav file");
let samples = wav.read().expect("Failed to read data");
write(out_path, &samples, wav.sample_rate(), wav.n_channels())
.expect("Failed to write data");
let mut wav: Wav<f32> = Wav::from_path(out_path).expect("Failed to open file wav file");
let samples: Samples<f32> = wav.read().expect("Failed to read data");
let expected: Vec<f32> = read_text_to_vec(expected_path).expect("failed to load from txt");
for (exp, act) in expected.iter().zip(samples.as_ref()) {
assert_approx_eq!(*exp as f64, *act as f64, 1e-4);
}
std::fs::remove_file(Path::new(&out_path)).unwrap();
}
#[test]
fn test_read() {
let input_path = "./test_resources/one_channel_i16.wav";
let expected_path = "./test_resources/one_channel_i16.txt";
let expected_sr = 16000;
let mut wav: Wav<i16> = Wav::from_path(input_path).expect("Failed to open file wav file");
let samples: Samples<i16> = wav.read().expect("Failed to read data");
let actual_sr = wav.sample_rate();
let expected: Vec<i16> = read_text_to_vec(expected_path).expect("failed to load from txt");
assert_eq!(expected_sr, actual_sr, "Sample rates do not match");
for (exp, act) in expected.iter().zip(samples.as_ref()) {
assert_eq!(*exp, *act, "Samples do not match");
}
}
use std::stringify;
macro_rules! read_tests {
($($T:ident), *) => {
$(
paste::item! {
#[test]
fn [<read_$T>]() {
let t_string: &str = stringify!($T);
let wav_str = format!("./test_resources/one_channel_{}.wav", t_string);
let expected_str = format!("./test_resources/one_channel_{}.txt", t_string);
let (sample_data, _): (Samples<$T>, i32) = match read::<$T, _>(&wav_str) {
Ok((s, sr)) => (s, sr),
Err(e) => {eprintln!("{}\n{}", wav_str, e); panic!("Failed to read wav file")}
};
let expected_data: Vec<$T> = match read_text_to_vec(Path::new(&expected_str)) {
Ok(w) => w,
Err(e) => {eprintln!("{}\n{}", wav_str, e); panic!("Failed to read txt file")}
};
for (expected, actual) in expected_data.iter().zip(sample_data.iter()) {
assert_eq!(*expected, *actual, "{} != {}", expected, actual);
}
}
}
)*
}
}
read_tests!(i16, i32, f32, f64);
macro_rules! write_tests {
($($T:ident), *) => {
$(
paste::item! {
#[test]
fn [<write_$T>]() {
if !Path::new(TEST_OUTPUT).exists() {
std::fs::create_dir(TEST_OUTPUT).unwrap();
}
let t_string: &str = stringify!($T);
let wav_str = format!("./test_resources/one_channel_{}.wav", t_string);
let expected_str = format!("./test_resources/one_channel_{}.txt", t_string);
let mut wav: Wav<$T> =
Wav::from_path(wav_str).expect("Failed to create wav file");
let expected_samples: Samples<$T> = Samples::from(
read_text_to_vec(&Path::new(&expected_str)).expect("Failed to read to vec"),
);
let out = format!("{}_one_channel_{}.wav", TEST_OUTPUT, t_string);
let out_path = Path::new(&out);
wav.write::<$T, _>(out_path)
.expect("Failed to write file");
let mut new_wav: Wav<$T> = Wav::<$T>::from_path(out_path).unwrap();
for (expected, actual) in expected_samples
.iter()
.zip(new_wav.read().unwrap().iter())
{
assert_eq!(expected, actual, "{} != {}", expected, actual);
}
std::fs::remove_file(Path::new(&out_path)).unwrap();
}
}
)*
};
}
write_tests!(i16, i32, f32, f64);
use crate::ConvertSlice;
#[test]
fn write_sin_wav() {
let fp = "./wav.wav";
let sr: i32 = 16000;
let duration = 10;
let mut samples: Vec<f32> = (0..sr * duration).map(|x| (x as f32 / sr as f32)).collect();
for sample in samples.iter_mut() {
*sample *= 440.0 * 2.0 * std::f32::consts::PI;
*sample = sample.sin();
*sample *= i16::MAX as f32;
}
let samples: Samples<f32> = Samples::from(samples.into_boxed_slice().convert_slice());
write(fp, &samples, sr, 1).unwrap();
std::fs::remove_file(fp).unwrap();
}
fn read_lines<P>(filename: P) -> std::io::Result<std::io::Lines<std::io::BufReader<File>>>
where
P: AsRef<Path>,
{
let file = File::open(filename)?;
Ok(std::io::BufReader::new(file).lines())
}
fn read_text_to_vec<T: FromStr, P: AsRef<Path>>(
fp: P,
) -> Result<Vec<T>, Box<dyn std::error::Error>>
where
<T as FromStr>::Err: std::error::Error + 'static,
{
let mut data = Vec::new();
let lines = read_lines(fp)?;
for line in lines {
let line = line?;
for sample in line.split(" ") {
let parsed_sample: T = match sample.trim().parse::<T>() {
Ok(num) => num,
Err(err) => {
eprintln!("Failed to parse {}", sample);
panic!("{}", err)
}
};
data.push(parsed_sample);
}
}
Ok(data)
}
}
#[cfg(feature = "ndarray")]
pub use conversion::{AsNdarray, IntoNdarray};