1use std::io::{self, Write};
2use std::path::Path;
3use symphonia::core::codecs::CodecParameters as SymphoniaCodecParameters;
4use symphonia::core::codecs::audio::{AudioCodecParameters, AudioDecoderOptions};
5use symphonia::core::errors::Error as SymphoniaError;
6use symphonia::core::formats::probe::Hint;
7use symphonia::core::formats::{FormatOptions, FormatReader, TrackType};
8use symphonia::core::io::MediaSourceStream;
9use symphonia::core::meta::MetadataOptions;
10
11use oxideav_core::{
12 AudioFrame, CodecId, CodecParameters, Frame, MediaType, Packet, RuntimeContext, SampleFormat,
13 StreamInfo, TimeBase,
14};
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AudioEncodeFormat {
19 Wav(WavBitDepth),
21 Flac(u16),
24 OggFlac(u16),
27 Mp3,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WavBitDepth {
36 Int16,
37 Int24,
38 Int32,
39 Float32,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum AudioDither {
46 #[default]
47 None,
48 Rectangular,
49 Triangular,
50}
51
52pub fn decode_audio_to_f32_interleaved_sync(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
59 decode_with_symphonia(path)
60}
61
62pub fn decode_audio_to_f32_interleaved_preferring_wav(
67 path: &Path,
68) -> io::Result<(Vec<f32>, usize, u32)> {
69 decode_audio_to_f32_interleaved_sync(path)
70}
71
72fn decode_with_symphonia(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
77 let file = std::fs::File::open(path)
78 .map_err(|e| io::Error::other(format!("Failed to open '{}': {e}", path.display())))?;
79 let mss = MediaSourceStream::new(Box::new(file), Default::default());
80
81 let mut hint = Hint::new();
82 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
83 hint.with_extension(ext);
84 }
85
86 let format_opts = FormatOptions::default();
87 let metadata_opts = MetadataOptions::default();
88 let decoder_opts = AudioDecoderOptions::default();
89
90 let mut format: Box<dyn FormatReader> = symphonia::default::get_probe()
91 .probe(&hint, mss, format_opts, metadata_opts)
92 .map_err(|e| {
93 io::Error::other(format!(
94 "Symphonia failed to probe format for '{}': {e}",
95 path.display()
96 ))
97 })?;
98
99 let track = format
100 .default_track(TrackType::Audio)
101 .or_else(|| format.tracks().first())
102 .ok_or_else(|| {
103 io::Error::other(format!("No usable audio track in '{}'", path.display()))
104 })?;
105
106 let codec_params: &AudioCodecParameters = track
107 .codec_params
108 .as_ref()
109 .and_then(SymphoniaCodecParameters::audio)
110 .ok_or_else(|| {
111 io::Error::other(format!("No usable audio track in '{}'", path.display()))
112 })?;
113
114 let channels = codec_params
115 .channels
116 .as_ref()
117 .map(|c| c.count())
118 .unwrap_or(1);
119 let sample_rate = codec_params.sample_rate.unwrap_or(48_000);
120 let track_id = track.id;
121
122 let mut decoder = symphonia::default::get_codecs()
123 .make_audio_decoder(codec_params, &decoder_opts)
124 .map_err(|e| {
125 io::Error::other(format!(
126 "Symphonia failed to create decoder for '{}': {e}",
127 path.display()
128 ))
129 })?;
130
131 let mut samples = Vec::new();
132
133 loop {
134 let packet = match format.next_packet() {
135 Ok(Some(packet)) => packet,
136 Ok(None) => break,
137 Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
138 break;
139 }
140 Err(e) => {
141 return Err(io::Error::other(format!(
142 "Symphonia read error for '{}': {e}",
143 path.display()
144 )));
145 }
146 };
147
148 if packet.track_id != track_id {
149 continue;
150 }
151
152 let decoded = decoder.decode(&packet).map_err(|e| {
153 io::Error::other(format!(
154 "Symphonia decode error for '{}': {e}",
155 path.display()
156 ))
157 })?;
158
159 let mut packet_samples = Vec::new();
160 decoded.copy_to_vec_interleaved(&mut packet_samples);
161 samples.extend_from_slice(&packet_samples);
162 }
163
164 if samples.is_empty() {
165 return Err(io::Error::other(format!(
166 "No samples decoded from '{}'",
167 path.display()
168 )));
169 }
170
171 Ok((samples, channels, sample_rate))
172}
173
174pub fn encode_audio_to_file(
186 path: &Path,
187 samples: &[f32],
188 channels: usize,
189 sample_rate: u32,
190 format: AudioEncodeFormat,
191 dither: AudioDither,
192) -> io::Result<()> {
193 let channels = channels.max(1);
194 if sample_rate == 0 {
195 return Err(io::Error::other("encode: sample_rate must be > 0"));
196 }
197 if channels > 8 {
198 return Err(io::Error::other(format!(
199 "encode: channel count {channels} exceeds the supported maximum of 8"
200 )));
201 }
202 if !samples.len().is_multiple_of(channels) {
203 return Err(io::Error::other(
204 "encode: sample slice length is not a multiple of channels",
205 ));
206 }
207
208 match format {
209 AudioEncodeFormat::Wav(depth) => {
210 encode_wav(path, samples, channels, sample_rate, depth, dither)
211 }
212 AudioEncodeFormat::Flac(bits) => {
213 encode_flac_to_file(path, samples, channels, sample_rate, bits, dither)
214 }
215 AudioEncodeFormat::OggFlac(bits) => {
216 encode_ogg_flac(path, samples, channels, sample_rate, bits, dither)
217 }
218 AudioEncodeFormat::Mp3 => encode_mp3(path, samples, channels, sample_rate, dither),
219 }
220}
221
222pub fn write_wav_f32(
224 path: &Path,
225 samples: &[f32],
226 channels: usize,
227 sample_rate: u32,
228) -> io::Result<()> {
229 encode_audio_to_file(
230 path,
231 samples,
232 channels,
233 sample_rate,
234 AudioEncodeFormat::Wav(WavBitDepth::Float32),
235 AudioDither::None,
236 )
237}
238
239pub fn write_flac(
241 path: &Path,
242 samples: &[f32],
243 channels: usize,
244 sample_rate: u32,
245 bits_per_sample: u16,
246) -> io::Result<()> {
247 encode_audio_to_file(
248 path,
249 samples,
250 channels,
251 sample_rate,
252 AudioEncodeFormat::Flac(bits_per_sample),
253 AudioDither::None,
254 )
255}
256
257fn encode_wav(
262 path: &Path,
263 samples: &[f32],
264 channels: usize,
265 sample_rate: u32,
266 depth: WavBitDepth,
267 dither: AudioDither,
268) -> io::Result<()> {
269 let (codec_id, sample_format) = match depth {
270 WavBitDepth::Int16 => ("pcm_s16le", SampleFormat::S16),
271 WavBitDepth::Int24 => ("pcm_s24le", SampleFormat::S24),
272 WavBitDepth::Int32 => ("pcm_s32le", SampleFormat::S32),
273 WavBitDepth::Float32 => ("pcm_f32le", SampleFormat::F32),
274 };
275 let bytes = pack_interleaved_samples(samples, sample_format, dither)?;
276
277 let mut ctx = RuntimeContext::new();
278 oxideav_basic::register(&mut ctx);
279
280 let stream = audio_stream_info(codec_id, channels, sample_rate, sample_format, None);
281 let file = std::fs::File::create(path)?;
282 let output: Box<dyn oxideav_core::WriteSeek> = Box::new(file);
283 let mut mux = ctx
284 .containers
285 .open_muxer("wav", output, std::slice::from_ref(&stream))
286 .map_err(oxideav_err_to_io)?;
287 mux.write_header().map_err(oxideav_err_to_io)?;
288 let packet = Packet::new(0, TimeBase::new(1, sample_rate as i64), bytes);
289 mux.write_packet(&packet).map_err(oxideav_err_to_io)?;
290 mux.write_trailer().map_err(oxideav_err_to_io)?;
291 Ok(())
292}
293
294fn encode_flac_to_file(
299 path: &Path,
300 samples: &[f32],
301 channels: usize,
302 sample_rate: u32,
303 bits_per_sample: u16,
304 dither: AudioDither,
305) -> io::Result<()> {
306 let (packets, output_params) =
307 encode_flac_packets(samples, channels, sample_rate, bits_per_sample, dither)?;
308
309 let mut ctx = RuntimeContext::new();
310 oxideav_flac::register(&mut ctx);
311
312 let stream = StreamInfo {
313 index: 0,
314 time_base: TimeBase::new(1, sample_rate as i64),
315 duration: None,
316 start_time: Some(0),
317 params: output_params,
318 };
319 let file = std::fs::File::create(path)?;
320 let output: Box<dyn oxideav_core::WriteSeek> = Box::new(file);
321 let mut mux = ctx
322 .containers
323 .open_muxer("flac", output, std::slice::from_ref(&stream))
324 .map_err(oxideav_err_to_io)?;
325 mux.write_header().map_err(oxideav_err_to_io)?;
326 for pkt in &packets {
327 mux.write_packet(pkt).map_err(oxideav_err_to_io)?;
328 }
329 mux.write_trailer().map_err(oxideav_err_to_io)?;
330 Ok(())
331}
332
333fn encode_flac_packets(
336 samples: &[f32],
337 channels: usize,
338 sample_rate: u32,
339 bits_per_sample: u16,
340 dither: AudioDither,
341) -> io::Result<(Vec<Packet>, CodecParameters)> {
342 let sample_format = flac_sample_format(bits_per_sample)?;
343 let bytes = pack_interleaved_samples(samples, sample_format, dither)?;
344
345 let mut ctx = RuntimeContext::new();
346 oxideav_flac::register(&mut ctx);
347
348 let params = audio_codec_params("flac", channels, sample_rate, sample_format, None);
349 let mut enc = ctx
350 .codecs
351 .first_encoder(¶ms)
352 .map_err(oxideav_err_to_io)?;
353
354 let frame = AudioFrame {
355 samples: (samples.len() / channels) as u32,
356 pts: Some(0),
357 data: vec![bytes],
358 };
359 enc.send_frame(&Frame::Audio(frame))
360 .map_err(oxideav_err_to_io)?;
361 enc.flush().map_err(oxideav_err_to_io)?;
362
363 let mut packets = Vec::new();
364 loop {
365 match enc.receive_packet() {
366 Ok(p) => packets.push(p),
367 Err(oxideav_core::Error::NeedMore) | Err(oxideav_core::Error::Eof) => break,
368 Err(e) => return Err(oxideav_err_to_io(e)),
369 }
370 }
371
372 Ok((packets, enc.output_params().clone()))
373}
374
375fn encode_ogg_flac(
376 path: &Path,
377 samples: &[f32],
378 channels: usize,
379 sample_rate: u32,
380 bits_per_sample: u16,
381 dither: AudioDither,
382) -> io::Result<()> {
383 let (packets, output_params) =
384 encode_flac_packets(samples, channels, sample_rate, bits_per_sample, dither)?;
385
386 let mut mapping = Vec::with_capacity(13);
389 mapping.push(0x7F);
390 mapping.extend_from_slice(b"FLAC");
391 mapping.push(0x01); mapping.push(0x00); mapping.extend_from_slice(&1u16.to_be_bytes());
395 mapping.extend_from_slice(b"fLaC");
396
397 let streaminfo = output_params.extradata;
398
399 let mut writer = oxideav_ogg::framing::PageWriter::new(0).with_page_target(4096);
400 writer.push_packet(&mapping, 0);
401 writer.flush_page();
402 writer.push_packet(&streaminfo, 0);
403 writer.flush_page();
404
405 for pkt in &packets {
406 let granule = pkt
407 .pts
408 .map(|pts| pts + pkt.duration.unwrap_or(0))
409 .unwrap_or(0);
410 writer.push_packet(&pkt.data, granule);
411 }
412
413 std::fs::write(path, writer.finish())?;
414 Ok(())
415}
416
417fn flac_sample_format(bits_per_sample: u16) -> io::Result<SampleFormat> {
418 match bits_per_sample {
419 8 => Ok(SampleFormat::U8),
420 16 => Ok(SampleFormat::S16),
421 24 => Ok(SampleFormat::S24),
422 32 => Ok(SampleFormat::S32),
423 _ => Err(io::Error::other(format!(
424 "FLAC bit depth {bits_per_sample} not supported (use 8, 16, 24 or 32)"
425 ))),
426 }
427}
428
429fn encode_mp3(
434 path: &Path,
435 samples: &[f32],
436 channels: usize,
437 sample_rate: u32,
438 dither: AudioDither,
439) -> io::Result<()> {
440 if channels > 2 {
441 return Err(io::Error::other(
442 "MP3 encode: only mono and stereo are supported",
443 ));
444 }
445 let bitrate = mp3_default_bitrate(sample_rate, channels);
446 let bytes = pack_interleaved_samples(samples, SampleFormat::S16, dither)?;
447
448 let mut ctx = RuntimeContext::new();
449 oxideav_mp3::register(&mut ctx);
450
451 let params = audio_codec_params(
452 "mp3",
453 channels,
454 sample_rate,
455 SampleFormat::S16,
456 Some(bitrate as u64),
457 );
458 let mut enc = ctx
459 .codecs
460 .first_encoder(¶ms)
461 .map_err(oxideav_err_to_io)?;
462
463 let frame = AudioFrame {
464 samples: (samples.len() / channels) as u32,
465 pts: Some(0),
466 data: vec![bytes],
467 };
468 enc.send_frame(&Frame::Audio(frame))
469 .map_err(oxideav_err_to_io)?;
470 enc.flush().map_err(oxideav_err_to_io)?;
471
472 let mut file = std::fs::File::create(path)?;
473 loop {
474 match enc.receive_packet() {
475 Ok(pkt) => file.write_all(&pkt.data)?,
476 Err(oxideav_core::Error::NeedMore) | Err(oxideav_core::Error::Eof) => break,
477 Err(e) => return Err(oxideav_err_to_io(e)),
478 }
479 }
480 Ok(())
481}
482
483fn mp3_default_bitrate(sample_rate: u32, channels: usize) -> u32 {
484 if sample_rate >= 32_000 {
486 if channels >= 2 { 192_000 } else { 128_000 }
487 } else if sample_rate >= 16_000 {
488 if channels >= 2 { 96_000 } else { 64_000 }
490 } else {
491 if channels >= 2 { 48_000 } else { 32_000 }
493 }
494}
495
496fn audio_codec_params(
501 codec_id: &str,
502 channels: usize,
503 sample_rate: u32,
504 sample_format: SampleFormat,
505 bit_rate: Option<u64>,
506) -> CodecParameters {
507 let mut params = CodecParameters::audio(CodecId::new(codec_id));
508 params.media_type = MediaType::Audio;
509 params.channels = Some(channels as u16);
510 params.sample_rate = Some(sample_rate);
511 params.sample_format = Some(sample_format);
512 if let Some(br) = bit_rate {
513 params.bit_rate = Some(br);
514 }
515 params
516}
517
518fn audio_stream_info(
519 codec_id: &str,
520 channels: usize,
521 sample_rate: u32,
522 sample_format: SampleFormat,
523 bit_rate: Option<u64>,
524) -> StreamInfo {
525 let params = audio_codec_params(codec_id, channels, sample_rate, sample_format, bit_rate);
526 StreamInfo {
527 index: 0,
528 time_base: TimeBase::new(1, sample_rate as i64),
529 duration: None,
530 start_time: Some(0),
531 params,
532 }
533}
534
535fn pack_interleaved_samples(
536 samples: &[f32],
537 format: SampleFormat,
538 dither: AudioDither,
539) -> io::Result<Vec<u8>> {
540 let bytes_per_sample = format.bytes_per_sample();
541 let mut out = Vec::with_capacity(samples.len().saturating_mul(bytes_per_sample));
542 let mut rng = DitherRng::new(0x1234_5678_9abc_defe);
543
544 for &sample in samples {
545 let s = sample.clamp(-1.0, 1.0);
546 match format {
547 SampleFormat::U8 => {
548 let v = ((s + 1.0) * 127.5 + dither_offset(&mut rng, dither)).round() as u8;
549 out.push(v);
550 }
551 SampleFormat::S16 => {
552 let scale = i16::MAX as f32;
553 let q = quantize_with_dither(s, scale, &mut rng, dither)
554 .round()
555 .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
556 out.extend_from_slice(&q.to_le_bytes());
557 }
558 SampleFormat::S24 => {
559 let scale = 8_388_607.0;
560 let q = quantize_with_dither(s, scale, &mut rng, dither)
561 .round()
562 .clamp(-8_388_608.0, 8_388_607.0) as i32;
563 let b = q.to_le_bytes();
564 out.extend_from_slice(&b[..3]);
565 }
566 SampleFormat::S32 => {
567 let scale = i32::MAX as f32;
568 let q = quantize_with_dither(s, scale, &mut rng, dither)
569 .round()
570 .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
571 out.extend_from_slice(&q.to_le_bytes());
572 }
573 SampleFormat::F32 => {
574 out.extend_from_slice(&s.to_le_bytes());
575 }
576 _ => {
577 return Err(io::Error::other(format!(
578 "unsupported sample format {format:?}"
579 )));
580 }
581 }
582 }
583 Ok(out)
584}
585
586fn quantize_with_dither(sample: f32, scale: f32, rng: &mut DitherRng, dither: AudioDither) -> f32 {
587 let d = dither_offset(rng, dither);
588 (sample + d / scale).clamp(-1.0, 1.0) * scale
589}
590
591fn dither_offset(rng: &mut DitherRng, dither: AudioDither) -> f32 {
592 match dither {
593 AudioDither::None => 0.0,
594 AudioDither::Rectangular => rng.uniform_half(),
595 AudioDither::Triangular => rng.uniform_half() + rng.uniform_half(),
596 }
597}
598
599fn oxideav_err_to_io(e: oxideav_core::Error) -> io::Error {
600 io::Error::other(format!("OxideAV error: {e}"))
601}
602
603struct DitherRng {
605 state: u64,
606}
607
608impl DitherRng {
609 fn new(seed: u64) -> Self {
610 Self { state: seed.max(1) }
611 }
612
613 fn next_u64(&mut self) -> u64 {
614 self.state ^= self.state >> 12;
616 self.state ^= self.state << 25;
617 self.state ^= self.state >> 27;
618 self.state.wrapping_mul(0x2545_f491_4f6c_dd1d)
619 }
620
621 fn uniform_half(&mut self) -> f32 {
623 let u = self.next_u64() >> 32;
624 (u as f32 / 4_294_967_296.0) - 0.5
625 }
626}
627
628#[cfg(test)]
629mod tests {
630 use super::*;
631
632 #[test]
633 fn decode_stereo_wav_returns_interleaved_samples() {
634 let path =
635 std::env::temp_dir().join(format!("maolan_stereo_decode_{}.wav", std::process::id()));
636 write_test_wav_f32(
637 &path,
638 &[
639 0.10, 0.60, 0.20, 0.70, 0.30, 0.80, 0.40, 0.90,
643 ],
644 2,
645 48_000,
646 )
647 .expect("write test wav");
648
649 let (samples, channels, sample_rate) =
650 decode_audio_to_f32_interleaved_sync(&path).expect("decode test wav");
651 let _ = std::fs::remove_file(&path);
652
653 assert_eq!(channels, 2);
654 assert_eq!(sample_rate, 48_000);
655 assert_eq!(samples.len(), 8);
656 for (actual, expected) in samples
657 .iter()
658 .zip([0.10, 0.60, 0.20, 0.70, 0.30, 0.80, 0.40, 0.90])
659 {
660 assert!((actual - expected).abs() < 1.0e-6);
661 }
662 }
663
664 fn write_test_wav_f32(
665 path: &Path,
666 samples: &[f32],
667 channels: usize,
668 sample_rate: u32,
669 ) -> io::Result<()> {
670 let bytes_per_sample = 4usize;
671 let block_align = (channels * bytes_per_sample) as u16;
672 let byte_rate = sample_rate * u32::from(block_align);
673 let data_size = samples.len() * bytes_per_sample;
674 let riff_size = 36 + data_size as u32;
675
676 let mut file = std::fs::File::create(path)?;
677 file.write_all(b"RIFF")?;
678 file.write_all(&riff_size.to_le_bytes())?;
679 file.write_all(b"WAVE")?;
680 file.write_all(b"fmt ")?;
681 file.write_all(&16u32.to_le_bytes())?;
682 file.write_all(&3u16.to_le_bytes())?;
683 file.write_all(&(channels as u16).to_le_bytes())?;
684 file.write_all(&sample_rate.to_le_bytes())?;
685 file.write_all(&byte_rate.to_le_bytes())?;
686 file.write_all(&block_align.to_le_bytes())?;
687 file.write_all(&32u16.to_le_bytes())?;
688 file.write_all(b"data")?;
689 file.write_all(&(data_size as u32).to_le_bytes())?;
690 for sample in samples {
691 file.write_all(&sample.to_le_bytes())?;
692 }
693 Ok(())
694 }
695}