#![warn(clippy::unit_cmp)] #![warn(clippy::match_same_arms)] #![allow(clippy::result_large_err)] #![allow(clippy::missing_const_for_fn)] #![allow(clippy::collapsible_if)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::needless_borrows_for_generic_args)] #![allow(clippy::if_same_then_else)] #![allow(clippy::unnecessary_cast)] #![allow(clippy::identity_op)]
#![warn(clippy::inefficient_to_string)] #![warn(clippy::map_clone)] #![warn(clippy::unnecessary_to_owned)] #![warn(clippy::large_stack_arrays)] #![warn(clippy::box_collection)] #![warn(clippy::vec_box)] #![warn(clippy::needless_collect)]
#![warn(clippy::redundant_clone)] #![warn(clippy::identity_op)] #![warn(clippy::needless_return)] #![warn(clippy::let_unit_value)] #![warn(clippy::manual_map)] #![warn(clippy::unwrap_used)] #![warn(clippy::panic)]
#![warn(clippy::missing_panics_doc)] #![warn(clippy::missing_safety_doc)] #![warn(clippy::missing_const_for_fn)] #![allow(clippy::too_many_arguments)]
pub mod error;
pub mod traits;
pub mod types;
#[cfg(feature = "wav")]
pub mod wav;
#[cfg(feature = "wav")]
pub use crate::wav::{StreamedWavFile, StreamedWavWriter, wav_file::WavFile};
#[cfg(feature = "flac")]
pub mod flac;
#[cfg(feature = "flac")]
pub use crate::flac::CompressionLevel;
#[cfg(feature = "numpy")]
pub mod python;
#[cfg(feature = "numpy")]
pub use crate::python::read_pyarray;
use std::{
fs::File,
io::{BufReader, BufWriter, Read, Seek, Write},
num::NonZeroU32,
path::Path,
};
use audio_samples::{
AudioSamples, operations::ResamplingQuality, resample, traits::StandardSample,
};
pub use crate::{
error::{AudioIOError, AudioIOResult},
traits::{AudioFile, AudioFileMetadata, AudioFileRead, AudioStreamReader},
types::{BaseAudioInfo, FileType, OpenOptions, ValidatedSampleType},
};
pub(crate) const MAX_WAV_SIZE: u64 = 2 * 1024 * 1024 * 1024; pub(crate) const MAX_MMAP_SIZE: u64 = 512 * 1024 * 1024;
pub trait ReadSeek: Read + Seek {}
impl<RS> ReadSeek for RS where RS: Read + Seek {}
pub trait WriteSeek: Write + Seek {}
impl<WS> WriteSeek for WS where WS: Write + Seek {}
pub fn info<P: AsRef<Path>>(fp: P) -> AudioIOResult<BaseAudioInfo> {
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled to read WAV files",
));
#[cfg(feature = "wav")]
{
let wav_file = WavFile::open_metadata(path)?;
wav_file.base_info()
}
}
FileType::FLAC => {
#[cfg(not(feature = "flac"))]
return Err(crate::error::AudioIOError::missing_feature(
"'flac' feature must be enabled to read FLAC files",
));
#[cfg(feature = "flac")]
{
Err(crate::error::AudioIOError::unsupported_format(
"FLAC info extraction not yet implemented",
))
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported file format: {other:?}"
))),
}
}
pub fn read<P, T>(fp: P) -> AudioIOResult<AudioSamples<'static, T>>
where
P: AsRef<Path>,
T: StandardSample + 'static,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled to read WAV files",
));
#[cfg(feature = "wav")]
{
let wav_file = WavFile::open_with_options(path, OpenOptions::default())?;
let samples = wav_file.read::<T>()?;
Ok(samples.into_owned())
}
}
FileType::FLAC => {
#[cfg(not(feature = "flac"))]
return Err(crate::error::AudioIOError::missing_feature(
"'flac' feature must be enabled to read FLAC files",
));
#[cfg(feature = "flac")]
{
Err(crate::error::AudioIOError::unsupported_format(
"FLAC reading not yet implemented",
))
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported file format: {other:?}"
))),
}
}
pub fn read_and_resample<P, T>(
fp: P,
target_sr: NonZeroU32,
quality: Option<ResamplingQuality>,
) -> AudioIOResult<AudioSamples<'static, T>>
where
P: AsRef<Path>,
T: StandardSample,
{
let signal = read(fp)?;
resample::<T>(
&signal,
target_sr,
quality.unwrap_or(ResamplingQuality::Fast),
)
.map_err(AudioIOError::AudioSamples)
}
#[cfg(feature = "wav")]
pub fn open_streamed<P>(fp: P) -> AudioIOResult<StreamedWavFile<BufReader<File>>>
where
P: AsRef<Path>,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled for WAV streaming",
));
#[cfg(feature = "wav")]
{
let file = File::open(path)?;
let reader = std::io::BufReader::new(file);
StreamedWavFile::new_with_path(reader, path.to_path_buf())
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported file format for streaming: {other:?}"
))),
}
}
#[cfg(feature = "wav")]
pub fn open_streamed_reader<R>(reader: R) -> AudioIOResult<wav::StreamedWavFile<R>>
where
R: ReadSeek,
{
wav::StreamedWavFile::new(reader)
}
pub fn open_streamed_dyn<P>(fp: P) -> AudioIOResult<Box<dyn AudioStreamReader>>
where
P: AsRef<Path>,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled for WAV streaming",
));
#[cfg(feature = "wav")]
{
let file = File::open(path)?;
let reader = BufReader::new(file);
let streamed = StreamedWavFile::new_with_path(reader, path.to_path_buf())?;
Ok(Box::new(streamed))
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported file format for streaming: {other:?}"
))),
}
}
#[cfg(feature = "wav")]
pub fn create_streamed<P>(
fp: P,
channels: u16,
sample_rate: u32,
sample_type: ValidatedSampleType,
) -> AudioIOResult<StreamedWavWriter<BufWriter<File>>>
where
P: AsRef<Path>,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled for WAV writing",
));
#[cfg(feature = "wav")]
{
let file = File::create(path)?;
let writer = BufWriter::new(file);
match sample_type {
ValidatedSampleType::U8 | ValidatedSampleType::I16 => {
StreamedWavWriter::new_i16(writer, channels, sample_rate)
}
ValidatedSampleType::I24 => {
StreamedWavWriter::new_i24(writer, channels, sample_rate)
}
ValidatedSampleType::I32 => {
StreamedWavWriter::new_i32(writer, channels, sample_rate)
}
ValidatedSampleType::F32 => {
StreamedWavWriter::new_f32(writer, channels, sample_rate)
}
ValidatedSampleType::F64 => {
StreamedWavWriter::new_f64(writer, channels, sample_rate)
}
}
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported output format for streaming write: {other:?}"
))),
}
}
#[cfg(feature = "wav")]
pub fn create_streamed_writer<W>(
writer: W,
channels: u16,
sample_rate: u32,
sample_type: ValidatedSampleType,
) -> AudioIOResult<StreamedWavWriter<W>>
where
W: WriteSeek,
{
match sample_type {
ValidatedSampleType::U8 | ValidatedSampleType::I16 => {
StreamedWavWriter::new_i16(writer, channels, sample_rate)
}
ValidatedSampleType::I24 => StreamedWavWriter::new_i24(writer, channels, sample_rate),
ValidatedSampleType::I32 => StreamedWavWriter::new_i32(writer, channels, sample_rate),
ValidatedSampleType::F32 => StreamedWavWriter::new_f32(writer, channels, sample_rate),
ValidatedSampleType::F64 => StreamedWavWriter::new_f64(writer, channels, sample_rate),
}
}
pub fn open<P>(fp: P) -> AudioIOResult<Box<dyn AudioFile>>
where
P: AsRef<Path>,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled to open WAV files",
));
#[cfg(feature = "wav")]
{
let wav_file = WavFile::open_with_options(path, OpenOptions::default())?;
Ok(Box::new(wav_file))
}
}
FileType::FLAC => {
#[cfg(not(feature = "flac"))]
return Err(crate::error::AudioIOError::missing_feature(
"'flac' feature must be enabled to open FLAC files",
));
#[cfg(feature = "flac")]
{
Err(crate::error::AudioIOError::unsupported_format(
"FLAC opening not yet implemented",
))
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported file format: {other:?}"
))),
}
}
pub fn write<P, T>(fp: P, audio: &AudioSamples<T>) -> AudioIOResult<()>
where
P: AsRef<Path>,
T: StandardSample + 'static,
{
let path = fp.as_ref();
match FileType::from_path(path) {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled to write WAV files",
));
#[cfg(feature = "wav")]
{
let file = std::fs::File::create(path)?;
let buf_writer = std::io::BufWriter::new(file);
crate::wav::wav_file::write_wav(buf_writer, audio)
}
}
FileType::FLAC => {
#[cfg(not(feature = "flac"))]
return Err(crate::error::AudioIOError::missing_feature(
"'flac' feature must be enabled to write FLAC files",
));
#[cfg(feature = "flac")]
{
let file = std::fs::File::create(path)?;
let buf_writer = std::io::BufWriter::new(file);
crate::flac::write_flac(buf_writer, audio, CompressionLevel::default())
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported format: {other:?}"
))),
}
}
pub fn write_with<T, W>(writer: W, audio: &AudioSamples<T>, format: FileType) -> AudioIOResult<()>
where
T: StandardSample + 'static,
W: Write,
{
match format {
FileType::WAV => {
#[cfg(not(feature = "wav"))]
return Err(crate::error::AudioIOError::missing_feature(
"'wav' feature must be enabled to write WAV files",
));
#[cfg(feature = "wav")]
{
crate::wav::wav_file::write_wav(writer, audio)
}
}
FileType::FLAC => {
#[cfg(not(feature = "flac"))]
return Err(crate::error::AudioIOError::missing_feature(
"'flac' feature must be enabled to write FLAC files",
));
#[cfg(feature = "flac")]
{
crate::flac::write_flac(writer, audio, CompressionLevel::default())
}
}
other => Err(crate::error::AudioIOError::unsupported_format(format!(
"Unsupported format for write_with: {other:?}"
))),
}
}
#[cfg(all(test, feature = "wav"))]
mod lib_tests {
use std::time::Duration;
use audio_samples::sample_rate;
use super::*;
#[test]
fn test_info_function() {
let info_result = info("resources/test.wav");
assert!(info_result.is_ok(), "Failed to get info from test WAV file");
let audio_info = info_result.expect("Expected successful info retrieval");
assert_eq!(audio_info.file_type, FileType::WAV);
assert!(
audio_info.sample_rate.get() > 0,
"Sample rate should be positive"
);
assert!(audio_info.channels > 0, "Channel count should be positive");
println!("Audio info: {audio_info:#}");
}
#[test]
fn test_read_function() {
let audio_result = read::<_, f32>("resources/test.wav");
assert!(audio_result.is_ok(), "Failed to read test WAV file");
let audio_samples = audio_result.expect("Expected successful audio read");
println!(
"Read {} samples at {} Hz",
audio_samples.len(),
audio_samples.sample_rate()
);
}
#[test]
fn test_open_function() {
let file_result = open("resources/test.wav");
assert!(file_result.is_ok(), "Failed to open test WAV file");
}
#[test]
fn test_write_function() {
use audio_samples::sine_wave;
use std::fs;
let sample_rate = sample_rate!(44100);
let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.1), sample_rate, 0.5);
let output_path = std::env::temp_dir().join("test_lib_write.wav");
write(&output_path, &sine_samples).expect("Failed to write WAV file");
assert!(
fs::metadata(&output_path).is_ok(),
"Output file should exist"
);
let read_back = read::<_, f32>(&output_path).expect("Failed to read back WAV file");
assert_eq!(read_back.sample_rate(), sample_rate);
assert_eq!(read_back.total_samples(), sine_samples.total_samples());
assert_eq!(read_back.num_channels(), sine_samples.num_channels());
let original_bytes = sine_samples.bytes().expect("bytes should be available");
let read_bytes = read_back.bytes().expect("bytes should be available");
assert_eq!(
original_bytes.as_slice().len(),
read_bytes.as_slice().len(),
"Audio data size should match"
);
let original_samples: &[f32] = bytemuck::cast_slice(original_bytes.as_slice());
let read_samples: &[f32] = bytemuck::cast_slice(read_bytes.as_slice());
for (i, (orig, read)) in original_samples.iter().zip(read_samples.iter()).enumerate() {
let diff = (orig - read).abs();
assert!(
diff < 1e-6,
"Sample {i} differs too much: {orig} vs {read} (diff: {diff})"
);
}
fs::remove_file(&output_path).ok();
}
#[test]
fn test_write_with_function() {
use audio_samples::sine_wave;
use std::io::Cursor;
let sample_rate = sample_rate!(22050);
let sine_samples = sine_wave::<i16>(880.0, Duration::from_secs_f64(0.05), sample_rate, 0.8);
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
write_with(cursor, &sine_samples, FileType::WAV).expect("Failed to write with cursor");
assert!(
buffer.len() > 44,
"Buffer should contain WAV header and data"
);
assert_eq!(&buffer[0..4], b"RIFF", "Should start with RIFF header");
assert_eq!(&buffer[8..12], b"WAVE", "Should contain WAVE identifier");
let temp_file = std::env::temp_dir().join("test_write_with_buffer.wav");
std::fs::write(&temp_file, &buffer).expect("Failed to write buffer to temp file");
let read_back = read::<_, i16>(&temp_file).expect("Failed to read back WAV from buffer");
assert_eq!(read_back.sample_rate(), sample_rate);
assert_eq!(read_back.total_samples(), sine_samples.total_samples());
assert_eq!(read_back.num_channels(), sine_samples.num_channels());
let original_bytes = sine_samples.bytes().expect("bytes should be available");
let read_bytes = read_back.bytes().expect("bytes should be available");
assert_eq!(
original_bytes.as_slice(),
read_bytes.as_slice(),
"Audio data should match exactly for i16"
);
std::fs::remove_file(&temp_file).ok();
}
#[test]
fn test_write_with_format_parameter() {
use audio_samples::sine_wave;
use std::io::Cursor;
let sample_rate = sample_rate!(44100);
let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.01), sample_rate, 0.5);
let mut wav_buffer = Vec::new();
let wav_cursor = Cursor::new(&mut wav_buffer);
write_with(wav_cursor, &sine_samples, FileType::WAV).expect("Failed to write WAV format");
assert!(
wav_buffer.len() > 44,
"WAV buffer should contain header and data"
);
assert_eq!(&wav_buffer[0..4], b"RIFF", "Should start with RIFF header");
assert_eq!(
&wav_buffer[8..12],
b"WAVE",
"Should contain WAVE identifier"
);
let mut buffer = Vec::new();
let cursor = Cursor::new(&mut buffer);
let result = write_with(cursor, &sine_samples, FileType::MP3);
assert!(
result.is_err(),
"Should return error for unsupported format"
);
let error_msg = format!("{}", result.expect_err("Expected error"));
assert!(
error_msg.contains("Unsupported format"),
"Error should mention unsupported format"
);
}
#[test]
fn test_write_different_formats() {
use audio_samples::{AudioTypeConversion, sine_wave};
use std::fs;
let sample_rate = sample_rate!(48000);
let sine_base = sine_wave::<f32>(1000.0, Duration::from_secs_f64(0.02), sample_rate, 0.3);
let test_cases = vec![
("i16", std::env::temp_dir().join("test_format_i16.wav")),
("f32", std::env::temp_dir().join("test_format_f32.wav")),
];
for (format_name, output_path) in test_cases {
match format_name {
"i16" => {
let samples_i16 = sine_base.to_format::<i16>();
write(&output_path, &samples_i16).expect("Failed to write i16 WAV");
let read_back =
read::<_, i16>(&output_path).expect("Failed to read back i16 WAV");
assert_eq!(
read_back.sample_rate(),
sample_rate,
"Sample rate mismatch for i16"
);
assert_eq!(
read_back.total_samples(),
samples_i16.total_samples(),
"Sample count mismatch for i16"
);
let wav_info = info(&output_path).expect("Failed to get WAV info for i16");
assert_eq!(
wav_info.bits_per_sample, 16,
"Bits per sample should be 16 for i16"
);
assert_eq!(
wav_info.sample_type,
audio_samples::SampleType::I16,
"Sample type should be I16"
);
}
"f32" => {
write(&output_path, &sine_base).expect("Failed to write f32 WAV");
let read_back =
read::<_, f32>(&output_path).expect("Failed to read back f32 WAV");
assert_eq!(
read_back.sample_rate(),
sample_rate,
"Sample rate mismatch for f32"
);
assert_eq!(
read_back.total_samples(),
sine_base.total_samples(),
"Sample count mismatch for f32"
);
let wav_info = info(&output_path).expect("Failed to get WAV info for f32");
assert_eq!(
wav_info.bits_per_sample, 32,
"Bits per sample should be 32 for f32"
);
assert_eq!(
wav_info.sample_type,
audio_samples::SampleType::F32,
"Sample type should be F32"
);
}
_ => unreachable!("Unknown format"),
}
assert!(
fs::metadata(&output_path).is_ok(),
"File should exist for {format_name}"
);
fs::remove_file(&output_path).ok();
}
}
#[test]
fn test_unsupported_format_error() {
use audio_samples::sine_wave;
let sample_rate = sample_rate!(44100);
let sine_samples = sine_wave::<f32>(440.0, Duration::from_secs_f64(0.01), sample_rate, 0.1);
let result = write(std::env::temp_dir().join("test.mp3"), &sine_samples);
assert!(result.is_err(), "Should fail for unsupported format");
let error_msg = format!("{}", result.expect_err("Expected error"));
assert!(
error_msg.contains("Unsupported"),
"Error should mention unsupported format"
);
}
}