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}