Skip to main content

oxideav_basic/
wav.rs

1//! Minimal pure-Rust RIFF/WAVE container.
2//!
3//! Supports reading and writing linear PCM streams via the `pcm_*` codecs.
4
5use oxideav_core::{
6    CodecId, CodecParameters, CodecResolver, Error, MediaType, Packet, Result, SampleFormat,
7    StreamInfo, TimeBase,
8};
9use oxideav_core::{ContainerRegistry, Demuxer, Muxer, ReadSeek, WriteSeek};
10use std::io::{Read, Seek, SeekFrom, Write};
11
12pub fn register(reg: &mut ContainerRegistry) {
13    reg.register_demuxer("wav", open_demuxer);
14    reg.register_muxer("wav", open_muxer);
15    reg.register_extension("wav", "wav");
16    reg.register_extension("wave", "wav");
17    reg.register_probe("wav", probe);
18}
19
20/// `RIFF....WAVE` — unambiguous when present.
21fn probe(p: &oxideav_core::ProbeData) -> u8 {
22    if p.buf.len() < 12 {
23        return 0;
24    }
25    if &p.buf[0..4] == b"RIFF" && &p.buf[8..12] == b"WAVE" {
26        100
27    } else {
28        0
29    }
30}
31
32const FMT_PCM: u16 = 0x0001;
33const FMT_IEEE_FLOAT: u16 = 0x0003;
34const FMT_EXTENSIBLE: u16 = 0xFFFE;
35
36// Data GUIDs for WAVE_FORMAT_EXTENSIBLE subformats.
37const GUID_PCM: [u8; 16] = [
38    0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
39];
40const GUID_IEEE_FLOAT: [u8; 16] = [
41    0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71,
42];
43
44// --- Demuxer ---------------------------------------------------------------
45
46fn open_demuxer(
47    mut input: Box<dyn ReadSeek>,
48    _codecs: &dyn CodecResolver,
49) -> Result<Box<dyn Demuxer>> {
50    let mut hdr = [0u8; 12];
51    input.read_exact(&mut hdr)?;
52    if &hdr[0..4] != b"RIFF" || &hdr[8..12] != b"WAVE" {
53        return Err(Error::invalid("not a RIFF/WAVE file"));
54    }
55
56    // Walk chunks until we hit "data"; parse "fmt " and "LIST" along the way.
57    let mut fmt: Option<WaveFmt> = None;
58    let mut metadata: Vec<(String, String)> = Vec::new();
59    let data_offset: u64;
60    let data_size: u64;
61    loop {
62        let mut chdr = [0u8; 8];
63        input.read_exact(&mut chdr)?;
64        let id = &chdr[0..4];
65        let size = u32::from_le_bytes([chdr[4], chdr[5], chdr[6], chdr[7]]) as u64;
66        match id {
67            b"fmt " => {
68                let mut buf = vec![0u8; size as usize];
69                input.read_exact(&mut buf)?;
70                fmt = Some(parse_fmt(&buf)?);
71                if size % 2 == 1 {
72                    input.seek(SeekFrom::Current(1))?;
73                }
74            }
75            b"LIST" => {
76                let mut buf = vec![0u8; size as usize];
77                input.read_exact(&mut buf)?;
78                parse_list_chunk(&buf, &mut metadata);
79                if size % 2 == 1 {
80                    input.seek(SeekFrom::Current(1))?;
81                }
82            }
83            b"data" => {
84                data_offset = input.stream_position()?;
85                data_size = size;
86                break;
87            }
88            _ => {
89                let pad = size + (size % 2);
90                input.seek(SeekFrom::Current(pad as i64))?;
91            }
92        }
93    }
94    let fmt = fmt.ok_or_else(|| Error::invalid("WAV missing fmt chunk"))?;
95
96    let codec_id = resolve_codec(&fmt)?;
97    let sample_fmt = super::pcm::sample_format_for(&codec_id)
98        .ok_or_else(|| Error::unsupported(format!("unsupported WAV codec {}", codec_id)))?;
99
100    let time_base = TimeBase::new(1, fmt.sample_rate as i64);
101    let block_align = fmt.block_align.max(1) as u64;
102    let total_samples = data_size / block_align;
103    let duration_micros: i64 = if fmt.sample_rate > 0 {
104        (total_samples as i128 * 1_000_000 / fmt.sample_rate as i128) as i64
105    } else {
106        0
107    };
108
109    let mut params = CodecParameters::audio(codec_id);
110    params.channels = Some(fmt.channels);
111    params.sample_rate = Some(fmt.sample_rate);
112    params.sample_format = Some(sample_fmt);
113    params.bit_rate = Some(
114        (sample_fmt.bytes_per_sample() as u64)
115            * 8
116            * (fmt.channels as u64)
117            * (fmt.sample_rate as u64),
118    );
119
120    let stream = StreamInfo {
121        index: 0,
122        time_base,
123        duration: Some(total_samples as i64),
124        start_time: Some(0),
125        params,
126    };
127
128    Ok(Box::new(WavDemuxer {
129        input,
130        streams: vec![stream],
131        data_offset,
132        data_end: data_offset + data_size,
133        cursor: data_offset,
134        block_align,
135        chunk_frames: 1024,
136        samples_emitted: 0,
137        metadata,
138        duration_micros,
139    }))
140}
141
142/// Parse a RIFF LIST chunk body. If the list type is `INFO`, map its
143/// `IART`/`INAM`/... subchunks to standard key names (`artist`, `title`,
144/// …) and append to `out`.
145fn parse_list_chunk(buf: &[u8], out: &mut Vec<(String, String)>) {
146    if buf.len() < 4 {
147        return;
148    }
149    if &buf[0..4] != b"INFO" {
150        return;
151    }
152    let mut i = 4usize;
153    while i + 8 <= buf.len() {
154        let id: [u8; 4] = [buf[i], buf[i + 1], buf[i + 2], buf[i + 3]];
155        let size = u32::from_le_bytes([buf[i + 4], buf[i + 5], buf[i + 6], buf[i + 7]]) as usize;
156        i += 8;
157        if i + size > buf.len() {
158            break;
159        }
160        let raw = &buf[i..i + size];
161        let end = raw.iter().position(|&b| b == 0).unwrap_or(raw.len());
162        let value = String::from_utf8_lossy(&raw[..end]).trim().to_string();
163        let key = info_id_to_key(&id);
164        if !value.is_empty() {
165            if let Some(k) = key {
166                out.push((k.to_string(), value));
167            }
168        }
169        i += size;
170        if size % 2 == 1 {
171            i += 1;
172        }
173    }
174}
175
176fn info_id_to_key(id: &[u8; 4]) -> Option<&'static str> {
177    match id {
178        b"INAM" => Some("title"),
179        b"IART" => Some("artist"),
180        b"IPRD" => Some("album"),
181        b"ICMT" => Some("comment"),
182        b"ICRD" => Some("date"),
183        b"IGNR" => Some("genre"),
184        b"ICOP" => Some("copyright"),
185        b"IENG" => Some("engineer"),
186        b"ITCH" => Some("technician"),
187        b"ISFT" => Some("encoder"),
188        b"ISBJ" => Some("subject"),
189        b"ITRK" => Some("track"),
190        _ => None,
191    }
192}
193
194#[derive(Clone, Debug)]
195struct WaveFmt {
196    format_tag: u16,
197    channels: u16,
198    sample_rate: u32,
199    #[allow(dead_code)]
200    byte_rate: u32,
201    block_align: u16,
202    bits_per_sample: u16,
203    subformat: Option<[u8; 16]>,
204}
205
206fn parse_fmt(buf: &[u8]) -> Result<WaveFmt> {
207    if buf.len() < 16 {
208        return Err(Error::invalid("fmt chunk too small"));
209    }
210    let format_tag = u16::from_le_bytes([buf[0], buf[1]]);
211    let channels = u16::from_le_bytes([buf[2], buf[3]]);
212    let sample_rate = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
213    let byte_rate = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
214    let block_align = u16::from_le_bytes([buf[12], buf[13]]);
215    let bits_per_sample = u16::from_le_bytes([buf[14], buf[15]]);
216    let mut subformat = None;
217    if format_tag == FMT_EXTENSIBLE && buf.len() >= 40 {
218        let mut g = [0u8; 16];
219        g.copy_from_slice(&buf[24..40]);
220        subformat = Some(g);
221    }
222    Ok(WaveFmt {
223        format_tag,
224        channels,
225        sample_rate,
226        byte_rate,
227        block_align,
228        bits_per_sample,
229        subformat,
230    })
231}
232
233fn resolve_codec(fmt: &WaveFmt) -> Result<CodecId> {
234    let (is_float, bits) = match fmt.format_tag {
235        FMT_PCM => (false, fmt.bits_per_sample),
236        FMT_IEEE_FLOAT => (true, fmt.bits_per_sample),
237        FMT_EXTENSIBLE => {
238            let sub = fmt
239                .subformat
240                .ok_or_else(|| Error::invalid("extensible WAV missing subformat"))?;
241            let is_float = match sub {
242                GUID_PCM => false,
243                GUID_IEEE_FLOAT => true,
244                _ => {
245                    return Err(Error::unsupported(
246                        "unsupported WAVE_FORMAT_EXTENSIBLE subformat",
247                    ));
248                }
249            };
250            (is_float, fmt.bits_per_sample)
251        }
252        other => {
253            return Err(Error::unsupported(format!(
254                "unsupported WAV format tag 0x{:04x}",
255                other
256            )));
257        }
258    };
259    let name = match (is_float, bits) {
260        (false, 8) => "pcm_u8",
261        (false, 16) => "pcm_s16le",
262        (false, 24) => "pcm_s24le",
263        (false, 32) => "pcm_s32le",
264        (true, 32) => "pcm_f32le",
265        (true, 64) => "pcm_f64le",
266        (f, b) => {
267            return Err(Error::unsupported(format!(
268                "unsupported WAV bit depth: float={} bits={}",
269                f, b
270            )));
271        }
272    };
273    Ok(CodecId::new(name))
274}
275
276struct WavDemuxer {
277    input: Box<dyn ReadSeek>,
278    streams: Vec<StreamInfo>,
279    data_offset: u64,
280    data_end: u64,
281    cursor: u64,
282    block_align: u64,
283    chunk_frames: u64,
284    samples_emitted: i64,
285    metadata: Vec<(String, String)>,
286    duration_micros: i64,
287}
288
289impl Demuxer for WavDemuxer {
290    fn format_name(&self) -> &str {
291        "wav"
292    }
293
294    fn streams(&self) -> &[StreamInfo] {
295        &self.streams
296    }
297
298    fn next_packet(&mut self) -> Result<Packet> {
299        if self.cursor >= self.data_end {
300            return Err(Error::Eof);
301        }
302        let remaining = self.data_end - self.cursor;
303        let want_bytes = (self.chunk_frames * self.block_align).min(remaining);
304        let want_bytes = (want_bytes / self.block_align) * self.block_align;
305        if want_bytes == 0 {
306            return Err(Error::Eof);
307        }
308
309        // Ensure we're positioned correctly (if an upstream operation seeked us).
310        self.input.seek(SeekFrom::Start(self.cursor))?;
311        let mut buf = vec![0u8; want_bytes as usize];
312        self.input.read_exact(&mut buf)?;
313        self.cursor += want_bytes;
314
315        let stream = &self.streams[0];
316        let frames = want_bytes / self.block_align;
317        let pts = self.samples_emitted;
318        self.samples_emitted += frames as i64;
319
320        let mut pkt = Packet::new(0, stream.time_base, buf);
321        pkt.pts = Some(pts);
322        pkt.dts = Some(pts);
323        pkt.duration = Some(frames as i64);
324        pkt.flags.keyframe = true;
325        Ok(pkt)
326    }
327
328    fn seek_to(&mut self, stream_index: u32, pts: i64) -> Result<i64> {
329        if stream_index != 0 {
330            return Err(Error::invalid(format!(
331                "WAV: stream index {stream_index} out of range"
332            )));
333        }
334        // PCM is keyframe-only and frame-aligned: the target pts is a
335        // sample-index offset into the data chunk. Clamp to the valid
336        // range and translate to a byte offset.
337        let total_samples = (self.data_end - self.data_offset) / self.block_align;
338        let target = (pts.max(0) as u64).min(total_samples);
339        let new_cursor = self.data_offset + target * self.block_align;
340
341        self.input.seek(SeekFrom::Start(new_cursor))?;
342        self.cursor = new_cursor;
343        self.samples_emitted = target as i64;
344        Ok(target as i64)
345    }
346
347    fn metadata(&self) -> &[(String, String)] {
348        &self.metadata
349    }
350
351    fn duration_micros(&self) -> Option<i64> {
352        if self.duration_micros > 0 {
353            Some(self.duration_micros)
354        } else {
355            None
356        }
357    }
358}
359
360// --- Muxer -----------------------------------------------------------------
361
362fn open_muxer(output: Box<dyn WriteSeek>, streams: &[StreamInfo]) -> Result<Box<dyn Muxer>> {
363    if streams.len() != 1 {
364        return Err(Error::unsupported("WAV supports exactly one audio stream"));
365    }
366    let s = &streams[0];
367    if s.params.media_type != MediaType::Audio {
368        return Err(Error::invalid("WAV stream must be audio"));
369    }
370    let fmt = sample_format_for_params(&s.params)?;
371    let channels = s
372        .params
373        .channels
374        .ok_or_else(|| Error::invalid("WAV muxer: missing channels"))?;
375    let sample_rate = s
376        .params
377        .sample_rate
378        .ok_or_else(|| Error::invalid("WAV muxer: missing sample rate"))?;
379    Ok(Box::new(WavMuxer {
380        output,
381        channels,
382        sample_rate,
383        sample_format: fmt,
384        riff_size_offset: 0,
385        data_size_offset: 0,
386        data_bytes: 0,
387        header_written: false,
388        trailer_written: false,
389    }))
390}
391
392fn sample_format_for_params(p: &CodecParameters) -> Result<SampleFormat> {
393    p.sample_format
394        .or_else(|| super::pcm::sample_format_for(&p.codec_id))
395        .ok_or_else(|| Error::unsupported(format!("WAV: unknown PCM codec {}", p.codec_id)))
396}
397
398struct WavMuxer {
399    output: Box<dyn WriteSeek>,
400    channels: u16,
401    sample_rate: u32,
402    sample_format: SampleFormat,
403    riff_size_offset: u64,
404    data_size_offset: u64,
405    data_bytes: u64,
406    header_written: bool,
407    trailer_written: bool,
408}
409
410impl Muxer for WavMuxer {
411    fn format_name(&self) -> &str {
412        "wav"
413    }
414
415    fn write_header(&mut self) -> Result<()> {
416        if self.header_written {
417            return Err(Error::other("WAV header already written"));
418        }
419        let (format_tag, bits_per_sample) = match self.sample_format {
420            SampleFormat::U8 => (FMT_PCM, 8u16),
421            SampleFormat::S16 => (FMT_PCM, 16),
422            SampleFormat::S24 => (FMT_PCM, 24),
423            SampleFormat::S32 => (FMT_PCM, 32),
424            SampleFormat::F32 => (FMT_IEEE_FLOAT, 32),
425            SampleFormat::F64 => (FMT_IEEE_FLOAT, 64),
426            other => {
427                return Err(Error::unsupported(format!(
428                    "WAV muxer cannot write sample format {:?}",
429                    other
430                )));
431            }
432        };
433        let block_align = (bits_per_sample / 8) * self.channels;
434        let byte_rate = self.sample_rate * block_align as u32;
435
436        self.output.write_all(b"RIFF")?;
437        self.riff_size_offset = self.output.stream_position()?;
438        self.output.write_all(&0u32.to_le_bytes())?; // placeholder
439        self.output.write_all(b"WAVE")?;
440
441        self.output.write_all(b"fmt ")?;
442        self.output.write_all(&16u32.to_le_bytes())?;
443        self.output.write_all(&format_tag.to_le_bytes())?;
444        self.output.write_all(&self.channels.to_le_bytes())?;
445        self.output.write_all(&self.sample_rate.to_le_bytes())?;
446        self.output.write_all(&byte_rate.to_le_bytes())?;
447        self.output.write_all(&block_align.to_le_bytes())?;
448        self.output.write_all(&bits_per_sample.to_le_bytes())?;
449
450        self.output.write_all(b"data")?;
451        self.data_size_offset = self.output.stream_position()?;
452        self.output.write_all(&0u32.to_le_bytes())?; // placeholder
453
454        self.header_written = true;
455        Ok(())
456    }
457
458    fn write_packet(&mut self, packet: &Packet) -> Result<()> {
459        if !self.header_written {
460            return Err(Error::other("WAV muxer: write_header not called"));
461        }
462        self.output.write_all(&packet.data)?;
463        self.data_bytes += packet.data.len() as u64;
464        Ok(())
465    }
466
467    fn write_trailer(&mut self) -> Result<()> {
468        if self.trailer_written {
469            return Ok(());
470        }
471        // Pad data chunk to even length.
472        if self.data_bytes % 2 == 1 {
473            self.output.write_all(&[0u8])?;
474        }
475        let end = self.output.stream_position()?;
476
477        // Patch "data" chunk size.
478        let data_size_u32: u32 = self
479            .data_bytes
480            .try_into()
481            .map_err(|_| Error::other("WAV data chunk exceeds 4 GiB"))?;
482        self.output.seek(SeekFrom::Start(self.data_size_offset))?;
483        self.output.write_all(&data_size_u32.to_le_bytes())?;
484
485        // Patch "RIFF" size: total file size minus 8 (RIFF + size fields).
486        let riff_size_u32: u32 = (end - 8)
487            .try_into()
488            .map_err(|_| Error::other("WAV RIFF size exceeds 4 GiB"))?;
489        self.output.seek(SeekFrom::Start(self.riff_size_offset))?;
490        self.output.write_all(&riff_size_u32.to_le_bytes())?;
491
492        self.output.seek(SeekFrom::Start(end))?;
493        self.output.flush()?;
494        self.trailer_written = true;
495        Ok(())
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use super::*;
502    use oxideav_core::{CodecParameters, MediaType};
503
504    fn make_stream(fmt: SampleFormat, ch: u16, sr: u32) -> StreamInfo {
505        let mut params = CodecParameters::audio(super::super::pcm::codec_id_for(fmt).unwrap());
506        params.media_type = MediaType::Audio;
507        params.channels = Some(ch);
508        params.sample_rate = Some(sr);
509        params.sample_format = Some(fmt);
510        StreamInfo {
511            index: 0,
512            time_base: TimeBase::new(1, sr as i64),
513            duration: None,
514            start_time: Some(0),
515            params,
516        }
517    }
518
519    #[test]
520    fn round_trip_s16_mono() {
521        // Write then read back a small S16 mono WAV via the public demuxer/muxer paths.
522        let samples: Vec<i16> = (0..1000).map(|i| ((i * 32) - 16000) as i16).collect();
523        let mut payload = Vec::with_capacity(samples.len() * 2);
524        for s in &samples {
525            payload.extend_from_slice(&s.to_le_bytes());
526        }
527
528        // Mux to a temp file, then demux and compare.
529        let stream = make_stream(SampleFormat::S16, 1, 48_000);
530        let tmp = std::env::temp_dir().join("oxideav-basic-wav-test.wav");
531        {
532            let f = std::fs::File::create(&tmp).unwrap();
533            let ws: Box<dyn WriteSeek> = Box::new(f);
534            let mut mux = open_muxer(ws, std::slice::from_ref(&stream)).unwrap();
535            mux.write_header().unwrap();
536            let pkt = Packet::new(0, stream.time_base, payload.clone());
537            mux.write_packet(&pkt).unwrap();
538            mux.write_trailer().unwrap();
539        }
540        let rs: Box<dyn ReadSeek> = Box::new(std::fs::File::open(&tmp).unwrap());
541        let mut dmx = open_demuxer(rs, &oxideav_core::NullCodecResolver).unwrap();
542        assert_eq!(dmx.format_name(), "wav");
543        assert_eq!(dmx.streams().len(), 1);
544        assert_eq!(dmx.streams()[0].params.codec_id, CodecId::new("pcm_s16le"));
545        let mut out_bytes = Vec::new();
546        loop {
547            match dmx.next_packet() {
548                Ok(p) => out_bytes.extend_from_slice(&p.data),
549                Err(Error::Eof) => break,
550                Err(e) => panic!("demux error: {e}"),
551            }
552        }
553        assert_eq!(out_bytes, payload);
554    }
555}