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