Skip to main content

wav_codec/
lib.rs

1//! WAV (RIFF/WAVE) reader and writer over std I/O.
2//!
3//! `wav_codec` reads and writes the uncompressed WAV encodings that real
4//! files carry: PCM at 8, 16, 24, and 32 bits (packed or in wider
5//! containers via `WAVE_FORMAT_EXTENSIBLE`) and IEEE float at 32 and 64
6//! bits. Both directions work over the std traits, so any `io::Read`
7//! source decodes and any `io::Write + io::Seek` sink encodes.
8//!
9//! # Reading
10//!
11//! ```no_run
12//! use wav_codec::WavReader;
13//!
14//! let mut reader = WavReader::open("input.wav")?;
15//! let spec = reader.spec();
16//! println!("{} Hz, {} channels", spec.sample_rate, spec.channels);
17//! let samples: Vec<i16> = reader.samples::<i16>().collect::<Result<_, _>>()?;
18//! # Ok::<(), wav_codec::Error>(())
19//! ```
20//!
21//! Every sample item is a `Result`. A stream that ends inside a sample, or
22//! short of the declared `data` size, yields exactly one `Err` and then
23//! the iterator is fused, so corruption cannot pass as a short clean read.
24//!
25//! # Writing
26//!
27//! ```no_run
28//! use wav_codec::{SampleFormat, WavSpec, WavWriter};
29//!
30//! let spec = WavSpec {
31//!     channels: 1,
32//!     sample_rate: 44100,
33//!     bits_per_sample: 16,
34//!     sample_format: SampleFormat::Int,
35//! };
36//! let mut writer = WavWriter::create("sine.wav", spec)?;
37//! for n in 0..44100u32 {
38//!     let t = n as f32 / 44100.0;
39//!     let v = (t * 440.0 * 2.0 * std::f32::consts::PI).sin();
40//!     writer.write_sample((v * i16::MAX as f32) as i16)?;
41//! }
42//! writer.finalize()?;
43//! # Ok::<(), wav_codec::Error>(())
44//! ```
45//!
46//! `finalize` writes the pad byte for an odd `data` size and patches the
47//! RIFF and `data` sizes. An incomplete last frame returns
48//! [`Error::UnfinishedFrame`] after the header is patched to a whole
49//! number of frames. Dropping a writer runs the same steps and discards
50//! the result, so Drop cannot report `UnfinishedFrame`. Call `finalize`
51//! to observe that error.
52//!
53//! # Sample types
54//!
55//! | file encoding             | readable as            | writable from                  |
56//! |---------------------------|------------------------|--------------------------------|
57//! | 8-bit PCM (offset binary) | `i8`, `i16`, `i32`     | `i8`, `i16`, `i32` (in range)  |
58//! | 16-bit PCM                | `i16`, `i32`           | `i8`, `i16`, `i32` (in range)  |
59//! | 24-bit PCM, 3 or 4 bytes  | `i32`                  | `i8`, `i16`, `i32` (in range)  |
60//! | 32-bit PCM                | `i32`                  | `i8`, `i16`, `i32`             |
61//! | 32-bit float              | `f32`, `f64`           | `f32`                          |
62//! | 64-bit float              | `f64`                  | `f32`, `f64`                   |
63//!
64//! 8-bit files are unsigned on disk and exposed as `i8` with the value
65//! `stored_byte - 128`. A read with a type too narrow for the file, or a
66//! type of the wrong class, fails with [`Error::TooWide`].
67//!
68//! # Scope
69//!
70//! Out of scope, rejected with a clean error: compressed format tags
71//! (ADPCM, a-law, mu-law, MP3), `RF64`, `RIFX`, `W64`, and any
72//! `WAVE_FORMAT_EXTENSIBLE` GUID other than PCM and IEEE float. The crate
73//! is an adapter over `std::io`, so `no_std` is unsupported.
74
75#![forbid(unsafe_code)]
76#![warn(missing_docs)]
77#![warn(rust_2018_idioms)]
78
79mod read;
80mod sample;
81mod write;
82
83use std::fmt;
84use std::io;
85
86pub use read::{IntoSamples, Samples, WavReader};
87pub use sample::Sample;
88pub use write::WavWriter;
89
90pub(crate) const TAG_PCM: u16 = 0x0001;
91pub(crate) const TAG_IEEE_FLOAT: u16 = 0x0003;
92pub(crate) const TAG_EXTENSIBLE: u16 = 0xFFFE;
93
94pub(crate) const GUID_PCM: [u8; 16] = [
95    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
96];
97pub(crate) const GUID_IEEE_FLOAT: [u8; 16] = [
98    0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
99];
100
101/// How samples are encoded on disk.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
103pub enum SampleFormat {
104    /// Two's complement PCM (8-bit files are offset binary on disk).
105    Int,
106    /// IEEE 754 binary32 or binary64.
107    Float,
108}
109
110/// Stream parameters: channel count, sample rate, precision, and encoding.
111#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
112pub struct WavSpec {
113    /// Number of interleaved channels, at least 1.
114    pub channels: u16,
115    /// Frames per second, at least 1.
116    pub sample_rate: u32,
117    /// Valid bits per sample (24 for a 24-in-32 file).
118    pub bits_per_sample: u16,
119    /// Integer PCM or IEEE float.
120    pub sample_format: SampleFormat,
121}
122
123/// A [`WavSpec`] plus the container width, for files whose samples sit in
124/// containers wider than their valid bits.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
126pub struct WavSpecEx {
127    /// The stream parameters.
128    pub spec: WavSpec,
129    /// Bytes each sample occupies on disk (`block_align / channels`).
130    pub bytes_per_sample: u16,
131}
132
133/// Errors from reading or writing a WAV stream.
134#[derive(Debug)]
135pub enum Error {
136    /// The underlying reader or writer failed.
137    Io(io::Error),
138    /// The container is malformed (or a writer spec would produce one).
139    Format(&'static str),
140    /// The container is well formed but outside this crate's scope.
141    Unsupported(&'static str),
142    /// The sample type does not fit the file: too narrow to hold the
143    /// file's precision on read, the wrong class (integer against float)
144    /// in either direction, a value outside the file's range on write, or
145    /// an integer writer spec wider than 32 bits at construction.
146    TooWide,
147    /// The writer was finalized in the middle of a frame. Explicit
148    /// [`WavWriter::finalize`] returns this. Drop runs the same patch and
149    /// discards the error.
150    UnfinishedFrame,
151    /// The `data` chunk would exceed what the 32-bit size fields can hold.
152    TooLarge,
153}
154
155/// Result alias with this crate's [`Error`].
156pub type Result<T> = std::result::Result<T, Error>;
157
158impl fmt::Display for Error {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        match self {
161            Error::Io(e) => write!(f, "I/O error: {}", e),
162            Error::Format(msg) => write!(f, "malformed WAV: {}", msg),
163            Error::Unsupported(msg) => write!(f, "unsupported WAV: {}", msg),
164            Error::TooWide => f.write_str("sample type does not fit the file's sample format"),
165            Error::UnfinishedFrame => f.write_str("writer finalized in the middle of a frame"),
166            Error::TooLarge => f.write_str("data chunk would exceed the 32-bit RIFF size fields"),
167        }
168    }
169}
170
171impl std::error::Error for Error {
172    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
173        match self {
174            Error::Io(e) => Some(e),
175            _ => None,
176        }
177    }
178}
179
180impl From<io::Error> for Error {
181    fn from(e: io::Error) -> Self {
182        Error::Io(e)
183    }
184}