wav-codec 0.1.0

WAV (RIFF/WAVE) reader and writer over std I/O. PCM and IEEE-float WAV only. Compressed WAV (ADPCM, mu-law, A-law) not decoded.
Documentation
//! WAV (RIFF/WAVE) reader and writer over std I/O.
//!
//! `wav_codec` reads and writes the uncompressed WAV encodings that real
//! files carry: PCM at 8, 16, 24, and 32 bits (packed or in wider
//! containers via `WAVE_FORMAT_EXTENSIBLE`) and IEEE float at 32 and 64
//! bits. Both directions work over the std traits, so any `io::Read`
//! source decodes and any `io::Write + io::Seek` sink encodes.
//!
//! # Reading
//!
//! ```no_run
//! use wav_codec::WavReader;
//!
//! let mut reader = WavReader::open("input.wav")?;
//! let spec = reader.spec();
//! println!("{} Hz, {} channels", spec.sample_rate, spec.channels);
//! let samples: Vec<i16> = reader.samples::<i16>().collect::<Result<_, _>>()?;
//! # Ok::<(), wav_codec::Error>(())
//! ```
//!
//! Every sample item is a `Result`. A stream that ends inside a sample, or
//! short of the declared `data` size, yields exactly one `Err` and then
//! the iterator is fused, so corruption cannot pass as a short clean read.
//!
//! # Writing
//!
//! ```no_run
//! use wav_codec::{SampleFormat, WavSpec, WavWriter};
//!
//! let spec = WavSpec {
//!     channels: 1,
//!     sample_rate: 44100,
//!     bits_per_sample: 16,
//!     sample_format: SampleFormat::Int,
//! };
//! let mut writer = WavWriter::create("sine.wav", spec)?;
//! for n in 0..44100u32 {
//!     let t = n as f32 / 44100.0;
//!     let v = (t * 440.0 * 2.0 * std::f32::consts::PI).sin();
//!     writer.write_sample((v * i16::MAX as f32) as i16)?;
//! }
//! writer.finalize()?;
//! # Ok::<(), wav_codec::Error>(())
//! ```
//!
//! `finalize` writes the pad byte for an odd `data` size and patches the
//! RIFF and `data` sizes. An incomplete last frame returns
//! [`Error::UnfinishedFrame`] after the header is patched to a whole
//! number of frames. Dropping a writer runs the same steps and discards
//! the result, so Drop cannot report `UnfinishedFrame`. Call `finalize`
//! to observe that error.
//!
//! # Sample types
//!
//! | file encoding             | readable as            | writable from                  |
//! |---------------------------|------------------------|--------------------------------|
//! | 8-bit PCM (offset binary) | `i8`, `i16`, `i32`     | `i8`, `i16`, `i32` (in range)  |
//! | 16-bit PCM                | `i16`, `i32`           | `i8`, `i16`, `i32` (in range)  |
//! | 24-bit PCM, 3 or 4 bytes  | `i32`                  | `i8`, `i16`, `i32` (in range)  |
//! | 32-bit PCM                | `i32`                  | `i8`, `i16`, `i32`             |
//! | 32-bit float              | `f32`, `f64`           | `f32`                          |
//! | 64-bit float              | `f64`                  | `f32`, `f64`                   |
//!
//! 8-bit files are unsigned on disk and exposed as `i8` with the value
//! `stored_byte - 128`. A read with a type too narrow for the file, or a
//! type of the wrong class, fails with [`Error::TooWide`].
//!
//! # Scope
//!
//! Out of scope, rejected with a clean error: compressed format tags
//! (ADPCM, a-law, mu-law, MP3), `RF64`, `RIFX`, `W64`, and any
//! `WAVE_FORMAT_EXTENSIBLE` GUID other than PCM and IEEE float. The crate
//! is an adapter over `std::io`, so `no_std` is unsupported.

#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(rust_2018_idioms)]

mod read;
mod sample;
mod write;

use std::fmt;
use std::io;

pub use read::{IntoSamples, Samples, WavReader};
pub use sample::Sample;
pub use write::WavWriter;

pub(crate) const TAG_PCM: u16 = 0x0001;
pub(crate) const TAG_IEEE_FLOAT: u16 = 0x0003;
pub(crate) const TAG_EXTENSIBLE: u16 = 0xFFFE;

pub(crate) const GUID_PCM: [u8; 16] = [
    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];
pub(crate) const GUID_IEEE_FLOAT: [u8; 16] = [
    0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
];

/// How samples are encoded on disk.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum SampleFormat {
    /// Two's complement PCM (8-bit files are offset binary on disk).
    Int,
    /// IEEE 754 binary32 or binary64.
    Float,
}

/// Stream parameters: channel count, sample rate, precision, and encoding.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WavSpec {
    /// Number of interleaved channels, at least 1.
    pub channels: u16,
    /// Frames per second, at least 1.
    pub sample_rate: u32,
    /// Valid bits per sample (24 for a 24-in-32 file).
    pub bits_per_sample: u16,
    /// Integer PCM or IEEE float.
    pub sample_format: SampleFormat,
}

/// A [`WavSpec`] plus the container width, for files whose samples sit in
/// containers wider than their valid bits.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct WavSpecEx {
    /// The stream parameters.
    pub spec: WavSpec,
    /// Bytes each sample occupies on disk (`block_align / channels`).
    pub bytes_per_sample: u16,
}

/// Errors from reading or writing a WAV stream.
#[derive(Debug)]
pub enum Error {
    /// The underlying reader or writer failed.
    Io(io::Error),
    /// The container is malformed (or a writer spec would produce one).
    Format(&'static str),
    /// The container is well formed but outside this crate's scope.
    Unsupported(&'static str),
    /// The sample type does not fit the file: too narrow to hold the
    /// file's precision on read, the wrong class (integer against float)
    /// in either direction, a value outside the file's range on write, or
    /// an integer writer spec wider than 32 bits at construction.
    TooWide,
    /// The writer was finalized in the middle of a frame. Explicit
    /// [`WavWriter::finalize`] returns this. Drop runs the same patch and
    /// discards the error.
    UnfinishedFrame,
    /// The `data` chunk would exceed what the 32-bit size fields can hold.
    TooLarge,
}

/// Result alias with this crate's [`Error`].
pub type Result<T> = std::result::Result<T, Error>;

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Error::Io(e) => write!(f, "I/O error: {}", e),
            Error::Format(msg) => write!(f, "malformed WAV: {}", msg),
            Error::Unsupported(msg) => write!(f, "unsupported WAV: {}", msg),
            Error::TooWide => f.write_str("sample type does not fit the file's sample format"),
            Error::UnfinishedFrame => f.write_str("writer finalized in the middle of a frame"),
            Error::TooLarge => f.write_str("data chunk would exceed the 32-bit RIFF size fields"),
        }
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::Io(e) => Some(e),
            _ => None,
        }
    }
}

impl From<io::Error> for Error {
    fn from(e: io::Error) -> Self {
        Error::Io(e)
    }
}