Skip to main content

wav_codec/
write.rs

1use std::fmt;
2use std::fs::File;
3use std::io::{self, BufWriter, Seek, SeekFrom, Write};
4use std::path::Path;
5
6use crate::sample::Codec;
7use crate::{
8    Error, Result, Sample, SampleFormat, WavSpec, WavSpecEx, GUID_IEEE_FLOAT, GUID_PCM,
9    TAG_EXTENSIBLE, TAG_IEEE_FLOAT, TAG_PCM,
10};
11
12/// Size of the encode block flushed to the sink in one write.
13const BLOCK: usize = 64 * 1024;
14
15/// Block-buffered WAV encoder over any `io::Write + io::Seek`.
16///
17/// The header is written at construction with placeholder sizes. Samples
18/// are encoded into a 64 KiB block that is flushed whole. [`finalize`]
19/// writes the pad byte for an odd `data` size and patches the RIFF size,
20/// the `data` size, and (for float formats) the `fact` frame count.
21/// An incomplete last frame is truncated to whole frames in the header
22/// and returns [`Error::UnfinishedFrame`]. Dropping the writer runs the
23/// same steps and discards the result, so call [`finalize`] to observe
24/// that error.
25///
26/// Header layout by spec:
27///
28/// * integer PCM, up to 2 channels, valid bits filling the container:
29///   `fmt ` size 16.
30/// * IEEE float, up to 2 channels: `fmt ` size 18 plus a `fact` chunk.
31/// * a container wider than the valid bits, or more than 2 channels:
32///   `fmt ` size 40 (`WAVE_FORMAT_EXTENSIBLE`, channel mask 0), plus
33///   `fact` for float.
34///
35/// [`finalize`]: Self::finalize
36pub struct WavWriter<W: Write + Seek> {
37    inner: W,
38    spec: WavSpecEx,
39    codec: Codec,
40    block: Vec<u8>,
41    /// Bytes of sample data written or buffered.
42    data_len: u64,
43    /// Largest `data_len` the u32 size fields can describe.
44    max_data: u64,
45    /// Stream position of the `RIFF` tag.
46    base: u64,
47    /// Offset from `base` of the `fact` frame-count field.
48    fact_pos: Option<u64>,
49    /// Offset from `base` of the `data` size field.
50    data_size_pos: u64,
51    /// Offset from `base` of the first sample.
52    data_start: u64,
53    finalized: bool,
54}
55
56impl WavWriter<BufWriter<File>> {
57    /// Create (or truncate) a file and write the header for `spec`.
58    pub fn create<P: AsRef<Path>>(path: P, spec: WavSpec) -> Result<Self> {
59        let file = File::create(path)?;
60        WavWriter::new(BufWriter::new(file), spec)
61    }
62}
63
64impl<W: Write + Seek> WavWriter<W> {
65    /// Write the header for `spec` to `writer`, using the narrowest
66    /// container that holds `bits_per_sample`.
67    pub fn new(writer: W, spec: WavSpec) -> Result<Self> {
68        if spec.bits_per_sample == 0 {
69            return Err(Error::Format("zero bits per sample"));
70        }
71        let bits = u32::from(spec.bits_per_sample);
72        match spec.sample_format {
73            SampleFormat::Int => {
74                if bits > 32 {
75                    return Err(Error::TooWide);
76                }
77            }
78            SampleFormat::Float => {
79                if bits != 32 && bits != 64 {
80                    // A well-formed spec that is simply outside this crate's scope is
81                    // Unsupported, not a malformed-container Format error. This matches
82                    // new_with_spec_ex and the reader for the same condition.
83                    return Err(Error::Unsupported(
84                        "float samples other than 32-bit and 64-bit",
85                    ));
86                }
87            }
88        }
89        let bytes_per_sample = match spec.sample_format {
90            SampleFormat::Int => ((bits + 7) / 8) as u16,
91            SampleFormat::Float => (bits / 8) as u16,
92        };
93        WavWriter::new_with_spec_ex(
94            writer,
95            WavSpecEx {
96                spec,
97                bytes_per_sample,
98            },
99        )
100    }
101
102    /// Write the header for `spec` to `writer`, with an explicit container
103    /// width (for example 24 valid bits in 4-byte containers).
104    pub fn new_with_spec_ex(mut writer: W, spec: WavSpecEx) -> Result<Self> {
105        let codec = validate(&spec)?;
106        let channels = spec.spec.channels;
107        let container = spec.bytes_per_sample;
108        let bits = spec.spec.bits_per_sample;
109        let container_bits = container * 8;
110        let block_align = u32::from(channels) * u32::from(container);
111        if block_align > u32::from(u16::MAX) {
112            return Err(Error::Unsupported("block align does not fit in 16 bits"));
113        }
114        let byte_rate = match spec.spec.sample_rate.checked_mul(block_align) {
115            Some(rate) => rate,
116            None => return Err(Error::Unsupported("byte rate does not fit in 32 bits")),
117        };
118        let is_float = spec.spec.sample_format == SampleFormat::Float;
119        let extensible = container_bits != bits || channels > 2;
120
121        let mut header = Vec::with_capacity(80);
122        header.extend_from_slice(b"RIFF");
123        header.extend_from_slice(&0u32.to_le_bytes());
124        header.extend_from_slice(b"WAVE");
125        header.extend_from_slice(b"fmt ");
126        let (tag, fmt_len, fmt_bits) = if extensible {
127            (TAG_EXTENSIBLE, 40u32, container_bits)
128        } else if is_float {
129            (TAG_IEEE_FLOAT, 18u32, bits)
130        } else {
131            (TAG_PCM, 16u32, bits)
132        };
133        header.extend_from_slice(&fmt_len.to_le_bytes());
134        header.extend_from_slice(&tag.to_le_bytes());
135        header.extend_from_slice(&channels.to_le_bytes());
136        header.extend_from_slice(&spec.spec.sample_rate.to_le_bytes());
137        header.extend_from_slice(&byte_rate.to_le_bytes());
138        header.extend_from_slice(&(block_align as u16).to_le_bytes());
139        header.extend_from_slice(&fmt_bits.to_le_bytes());
140        if extensible {
141            header.extend_from_slice(&22u16.to_le_bytes());
142            header.extend_from_slice(&bits.to_le_bytes());
143            header.extend_from_slice(&0u32.to_le_bytes());
144            header.extend_from_slice(if is_float {
145                &GUID_IEEE_FLOAT
146            } else {
147                &GUID_PCM
148            });
149        } else if is_float {
150            header.extend_from_slice(&0u16.to_le_bytes());
151        }
152        let fact_pos = if is_float {
153            header.extend_from_slice(b"fact");
154            header.extend_from_slice(&4u32.to_le_bytes());
155            let pos = header.len() as u64;
156            header.extend_from_slice(&0u32.to_le_bytes());
157            Some(pos)
158        } else {
159            None
160        };
161        header.extend_from_slice(b"data");
162        let data_size_pos = header.len() as u64;
163        header.extend_from_slice(&0u32.to_le_bytes());
164        let data_start = header.len() as u64;
165
166        // file_len = data_start + data_len + pad, and riff_size = file_len - 8
167        // must fit u32. The pad byte is reserved up front.
168        let max_data = (u64::from(u32::MAX) + 7 - data_start).min(u64::from(u32::MAX));
169
170        let base = writer.stream_position()?;
171        writer.write_all(&header)?;
172
173        Ok(WavWriter {
174            inner: writer,
175            spec,
176            codec,
177            block: Vec::with_capacity(BLOCK),
178            data_len: 0,
179            max_data,
180            base,
181            fact_pos,
182            data_size_pos,
183            data_start,
184            finalized: false,
185        })
186    }
187
188    /// Encode one sample.
189    ///
190    /// Integer types write to integer files and float types to float
191    /// files. A mismatch, an `f64` into a 32-bit float file, or an integer
192    /// value outside the file's range is [`Error::TooWide`].
193    #[inline]
194    pub fn write_sample<S: Sample>(&mut self, sample: S) -> Result<()> {
195        self.check_write::<S>()?;
196        let width = self.codec.width() as u64;
197        if self.data_len + width > self.max_data {
198            return Err(Error::TooLarge);
199        }
200        if self.block.len() + self.codec.width() > BLOCK {
201            self.flush_block()?;
202        }
203        <S as crate::sample::SampleOps>::encode(sample, &self.codec, &mut self.block)?;
204        self.data_len += width;
205        Ok(())
206    }
207
208    /// Encode a slice of samples.
209    ///
210    /// Type and size checks happen before any sample is written. A
211    /// per-value range failure leaves the preceding samples written.
212    pub fn write_samples<S: Sample>(&mut self, samples: &[S]) -> Result<()> {
213        self.check_write::<S>()?;
214        let width = self.codec.width();
215        let total = samples.len() as u64 * width as u64;
216        if self.data_len + total > self.max_data {
217            return Err(Error::TooLarge);
218        }
219        for &sample in samples {
220            if self.block.len() + width > BLOCK {
221                self.flush_block()?;
222            }
223            <S as crate::sample::SampleOps>::encode(sample, &self.codec, &mut self.block)?;
224            self.data_len += width as u64;
225        }
226        Ok(())
227    }
228
229    /// Push buffered samples to the sink and flush it. Sizes in the header
230    /// stay as they are. [`finalize`](Self::finalize) updates them.
231    pub fn flush(&mut self) -> Result<()> {
232        self.flush_block()?;
233        self.inner.flush()?;
234        Ok(())
235    }
236
237    /// Write the pad byte if needed, patch the header sizes, and flush.
238    ///
239    /// Returns [`Error::UnfinishedFrame`] when the sample count is not a
240    /// multiple of the channel count. The header is patched to a whole
241    /// number of frames first so the container stays readable. Drop runs
242    /// these same steps and discards the result, so Drop cannot report
243    /// [`Error::UnfinishedFrame`]. Call this method to observe that error.
244    /// Sample bytes written before the incomplete final frame may remain in
245    /// the sink past the declared RIFF boundary. The declared sizes still
246    /// bound a reader, and a physical truncation is not possible over a
247    /// generic `Write + Seek` sink.
248    pub fn finalize(mut self) -> Result<()> {
249        self.finalize_inner()
250    }
251
252    /// Frames (samples per channel) written so far.
253    pub fn duration(&self) -> u32 {
254        self.len() / u32::from(self.spec.spec.channels)
255    }
256
257    /// Samples written so far, across all channels.
258    pub fn len(&self) -> u32 {
259        (self.data_len / self.codec.width() as u64) as u32
260    }
261
262    /// Whether no sample has been written yet.
263    pub fn is_empty(&self) -> bool {
264        self.data_len == 0
265    }
266
267    /// The stream parameters.
268    pub fn spec(&self) -> WavSpec {
269        self.spec.spec
270    }
271
272    /// The stream parameters plus the container width.
273    pub fn spec_ex(&self) -> WavSpecEx {
274        self.spec
275    }
276
277    fn check_write<S: Sample>(&self) -> Result<()> {
278        if <S as crate::sample::SampleOps>::accepts_write(&self.codec) {
279            Ok(())
280        } else {
281            Err(Error::TooWide)
282        }
283    }
284
285    fn flush_block(&mut self) -> Result<()> {
286        if !self.block.is_empty() {
287            self.inner.write_all(&self.block)?;
288            self.block.clear();
289        }
290        Ok(())
291    }
292
293    fn finalize_inner(&mut self) -> Result<()> {
294        self.finalized = true;
295        self.flush_block()?;
296        let block_align = u64::from(self.block_align());
297        let unfinished = self.data_len % block_align != 0;
298        if unfinished {
299            self.data_len -= self.data_len % block_align;
300            self.inner
301                .seek(SeekFrom::Start(self.base + self.data_start + self.data_len))?;
302        }
303        let pad = self.data_len & 1;
304        if pad == 1 {
305            self.inner.write_all(&[0])?;
306        }
307        let file_len = self.data_start + self.data_len + pad;
308        let riff_size = (file_len - 8) as u32;
309        let data_size = self.data_len as u32;
310        self.patch(4, riff_size)?;
311        self.patch(self.data_size_pos, data_size)?;
312        if let Some(pos) = self.fact_pos {
313            let frames = (self.data_len / block_align) as u32;
314            self.patch(pos, frames)?;
315        }
316        self.inner.seek(SeekFrom::Start(self.base + file_len))?;
317        self.inner.flush()?;
318        if unfinished {
319            return Err(Error::UnfinishedFrame);
320        }
321        Ok(())
322    }
323
324    fn patch(&mut self, offset: u64, value: u32) -> io::Result<()> {
325        self.inner.seek(SeekFrom::Start(self.base + offset))?;
326        self.inner.write_all(&value.to_le_bytes())
327    }
328
329    fn block_align(&self) -> u32 {
330        u32::from(self.spec.spec.channels) * u32::from(self.spec.bytes_per_sample)
331    }
332}
333
334impl<W: Write + Seek> Drop for WavWriter<W> {
335    fn drop(&mut self) {
336        if !self.finalized {
337            let _ = self.finalize_inner();
338        }
339    }
340}
341
342impl<W: Write + Seek> fmt::Debug for WavWriter<W> {
343    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
344        f.debug_struct("WavWriter")
345            .field("spec", &self.spec)
346            .field("data_len", &self.data_len)
347            .field("finalized", &self.finalized)
348            .finish_non_exhaustive()
349    }
350}
351
352fn validate(spec: &WavSpecEx) -> Result<Codec> {
353    let s = spec.spec;
354    let container = spec.bytes_per_sample;
355    if s.channels == 0 {
356        return Err(Error::Format("zero channels"));
357    }
358    if s.sample_rate == 0 {
359        return Err(Error::Format("zero sample rate"));
360    }
361    if s.bits_per_sample == 0 {
362        return Err(Error::Format("zero bits per sample"));
363    }
364    if container == 0 {
365        return Err(Error::Format("zero bytes per sample"));
366    }
367    match s.sample_format {
368        SampleFormat::Int => {
369            if s.bits_per_sample > 32 {
370                return Err(Error::TooWide);
371            }
372            if container > 4 {
373                return Err(Error::Unsupported("PCM containers wider than 4 bytes"));
374            }
375            if s.bits_per_sample > container * 8 {
376                return Err(Error::Format("bits per sample exceed the container width"));
377            }
378            if container == 1 && s.bits_per_sample != 8 {
379                return Err(Error::Format("8-bit PCM must declare 8 valid bits"));
380            }
381        }
382        SampleFormat::Float => {
383            let ok = (s.bits_per_sample == 32 && container == 4)
384                || (s.bits_per_sample == 64 && container == 8);
385            if !ok {
386                return Err(Error::Unsupported(
387                    "float samples other than 32-bit and 64-bit",
388                ));
389            }
390        }
391    }
392    Ok(Codec::new(s.sample_format, s.bits_per_sample, container))
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use std::io::Cursor;
399
400    fn spec16() -> WavSpec {
401        WavSpec {
402            channels: 1,
403            sample_rate: 8000,
404            bits_per_sample: 16,
405            sample_format: SampleFormat::Int,
406        }
407    }
408
409    #[test]
410    fn too_large_fires_before_the_size_fields_wrap() {
411        let mut w = WavWriter::new(Cursor::new(Vec::new()), spec16()).unwrap();
412        // riff_size = data_start + data_len + pad - 8 must fit u32.
413        assert_eq!(w.max_data, u64::from(u32::MAX) + 7 - 44);
414        w.data_len = w.max_data - 2;
415        w.write_sample(1i16).unwrap();
416        assert!(matches!(w.write_sample(1i16), Err(Error::TooLarge)));
417        assert!(matches!(w.write_samples(&[1i16]), Err(Error::TooLarge)));
418        assert!(matches!(w.write_samples::<i16>(&[]), Ok(())));
419        // The counter was forged, so keep Drop from patching sizes with it.
420        w.finalized = true;
421    }
422
423    #[test]
424    fn max_data_accounts_for_the_fact_chunk() {
425        let spec = WavSpec {
426            sample_format: SampleFormat::Float,
427            bits_per_sample: 32,
428            ..spec16()
429        };
430        let w = WavWriter::new(Cursor::new(Vec::new()), spec).unwrap();
431        assert_eq!(w.data_start, 58);
432        assert_eq!(w.max_data, u64::from(u32::MAX) + 7 - 58);
433    }
434}