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