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