1use 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
20fn 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
36const 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
44fn 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 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
142fn 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 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 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
360fn 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())?; 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())?; 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 if self.data_bytes % 2 == 1 {
473 self.output.write_all(&[0u8])?;
474 }
475 let end = self.output.stream_position()?;
476
477 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 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 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 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}