1use std::io::{self, Write};
2use std::path::{Path, PathBuf};
3use symphonia::core::codecs::CodecParameters as SymphoniaCodecParameters;
4use symphonia::core::codecs::audio::{AudioCodecParameters, AudioDecoder, 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)> {
61 match decode_with_oxideav(path) {
62 Ok(decoded) => Ok(decoded),
63 Err(oxideav_err) => decode_with_symphonia(path).map_err(|symphonia_err| {
64 io::Error::other(format!(
65 "Failed to decode '{}' with OxideAV ({oxideav_err}) \
66 or Symphonia ({symphonia_err})",
67 path.display()
68 ))
69 }),
70 }
71}
72
73pub fn decode_audio_to_f32_interleaved_preferring_wav(
79 path: &Path,
80) -> io::Result<(Vec<f32>, usize, u32)> {
81 decode_audio_to_f32_interleaved_sync(path)
82}
83
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct AudioFileInfo {
91 pub sample_rate: u32,
92 pub channels: usize,
93 pub frames: Option<u64>,
95}
96
97pub fn probe_audio_file(path: &Path) -> io::Result<AudioFileInfo> {
101 let format = probe_format(path)?;
102 let track = format
103 .default_track(TrackType::Audio)
104 .or_else(|| format.tracks().first())
105 .ok_or_else(|| {
106 io::Error::other(format!("No usable audio track in '{}'", path.display()))
107 })?;
108 let params: &AudioCodecParameters = track
109 .codec_params
110 .as_ref()
111 .and_then(SymphoniaCodecParameters::audio)
112 .ok_or_else(|| {
113 io::Error::other(format!("No usable audio track in '{}'", path.display()))
114 })?;
115 let sample_rate = params.sample_rate.unwrap_or(0);
116 if sample_rate == 0 {
117 return Err(io::Error::other(format!(
118 "No sample rate in '{}'",
119 path.display()
120 )));
121 }
122 let channels = params.channels.as_ref().map(|c| c.count()).unwrap_or(0);
123 if channels == 0 {
124 return Err(io::Error::other(format!(
125 "No channel count in '{}'",
126 path.display()
127 )));
128 }
129 let frames = track
130 .time_base
131 .and_then(|tb| {
132 track
133 .duration
134 .and_then(|duration| tb.calc_duration(duration))
135 })
136 .map(|time| (time.as_secs_f64() * f64::from(sample_rate)).round() as u64);
137 Ok(AudioFileInfo {
138 sample_rate,
139 channels,
140 frames,
141 })
142}
143
144#[derive(Debug, Clone, Default, PartialEq, Eq)]
146pub struct AudioMetadata {
147 pub artist: Option<String>,
148 pub title: Option<String>,
149 pub album: Option<String>,
150 pub track_number: Option<String>,
151 pub date: Option<String>,
152 pub genre: Option<String>,
153}
154
155pub fn read_audio_metadata(path: &Path) -> io::Result<AudioMetadata> {
160 use symphonia::core::meta::{RawValue, StandardTag};
161
162 fn raw_string(value: &RawValue) -> Option<String> {
163 match value {
164 RawValue::String(s) => {
165 let s = s.trim();
166 (!s.is_empty()).then(|| s.to_string())
167 }
168 RawValue::UnsignedInt(v) => Some(v.to_string()),
169 _ => None,
170 }
171 }
172
173 let mut format = probe_format(path)?;
174 let metadata = format.metadata();
175 let Some(revision) = metadata.current() else {
176 return Err(io::Error::other(format!(
177 "No metadata in '{}'",
178 path.display()
179 )));
180 };
181 let mut meta = AudioMetadata::default();
182 let mut tags: Vec<&symphonia::core::meta::Tag> = revision.media.tags.iter().collect();
184 for track_meta in &revision.per_track {
185 tags.extend(track_meta.metadata.tags.iter());
186 }
187 for tag in tags {
188 let (slot, value): (&mut Option<String>, Option<String>) = match tag.std.as_ref() {
189 Some(StandardTag::Artist(s)) | Some(StandardTag::Performer(s)) => {
190 (&mut meta.artist, Some(s.trim().to_string()))
191 }
192 Some(StandardTag::TrackTitle(s)) => (&mut meta.title, Some(s.trim().to_string())),
193 Some(StandardTag::Album(s)) => (&mut meta.album, Some(s.trim().to_string())),
194 Some(StandardTag::TrackNumber(n)) => (&mut meta.track_number, Some(n.to_string())),
195 Some(StandardTag::RecordingDate(s))
196 | Some(StandardTag::ReleaseDate(s))
197 | Some(StandardTag::OriginalReleaseDate(s)) => {
198 (&mut meta.date, Some(s.trim().to_string()))
199 }
200 Some(StandardTag::Genre(s)) => (&mut meta.genre, Some(s.trim().to_string())),
201 Some(_) => continue,
202 None => {
204 let key = tag.raw.key.to_ascii_lowercase();
205 let slot = match key.as_str() {
206 "artist" | "performer" => &mut meta.artist,
207 "title" | "tracktitle" => &mut meta.title,
208 "album" => &mut meta.album,
209 "tracknumber" | "track" => &mut meta.track_number,
210 "date" | "year" => &mut meta.date,
211 "genre" => &mut meta.genre,
212 _ => continue,
213 };
214 (slot, raw_string(&tag.raw.value))
215 }
216 };
217 if slot.is_none()
218 && let Some(value) = value
219 && !value.is_empty()
220 {
221 *slot = Some(value);
222 }
223 }
224 Ok(meta)
225}
226
227fn probe_format(path: &Path) -> io::Result<Box<dyn FormatReader>> {
228 let file = std::fs::File::open(path)
229 .map_err(|e| io::Error::other(format!("Failed to open '{}': {e}", path.display())))?;
230 let mss = MediaSourceStream::new(Box::new(file), Default::default());
231 let mut hint = Hint::new();
232 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
233 hint.with_extension(ext);
234 }
235 symphonia::default::get_probe()
236 .probe(
237 &hint,
238 mss,
239 FormatOptions::default(),
240 MetadataOptions::default(),
241 )
242 .map_err(|e| {
243 io::Error::other(format!(
244 "Symphonia failed to probe format for '{}': {e}",
245 path.display()
246 ))
247 })
248}
249
250type SymphoniaStreamParts = (
253 Box<dyn FormatReader>,
254 Box<dyn AudioDecoder>,
255 u32,
256 usize,
257 u32,
258);
259
260pub struct StreamingDecoder {
264 path: PathBuf,
265 format: Box<dyn FormatReader>,
266 decoder: Box<dyn AudioDecoder>,
267 track_id: u32,
268 channels: usize,
269 sample_rate: u32,
270 pending: Vec<f32>,
272}
273
274impl StreamingDecoder {
275 pub fn new(path: &Path) -> io::Result<Self> {
276 let (format, decoder, track_id, channels, sample_rate) = Self::open(path)?;
277 Ok(Self {
278 path: path.to_path_buf(),
279 format,
280 decoder,
281 track_id,
282 channels,
283 sample_rate,
284 pending: Vec::new(),
285 })
286 }
287
288 fn open(path: &Path) -> io::Result<SymphoniaStreamParts> {
289 let format = probe_format(path)?;
290 let track = format
291 .default_track(TrackType::Audio)
292 .or_else(|| format.tracks().first())
293 .ok_or_else(|| {
294 io::Error::other(format!("No usable audio track in '{}'", path.display()))
295 })?;
296 let codec_params: &AudioCodecParameters = track
297 .codec_params
298 .as_ref()
299 .and_then(SymphoniaCodecParameters::audio)
300 .ok_or_else(|| {
301 io::Error::other(format!("No usable audio track in '{}'", path.display()))
302 })?;
303 let channels = codec_params
304 .channels
305 .as_ref()
306 .map(|c| c.count())
307 .unwrap_or(1);
308 let sample_rate = codec_params.sample_rate.unwrap_or(48_000);
309 let track_id = track.id;
310 let decoder = symphonia::default::get_codecs()
311 .make_audio_decoder(codec_params, &AudioDecoderOptions::default())
312 .map_err(|e| {
313 io::Error::other(format!(
314 "Symphonia failed to create decoder for '{}': {e}",
315 path.display()
316 ))
317 })?;
318 Ok((format, decoder, track_id, channels, sample_rate))
319 }
320
321 pub fn channels(&self) -> usize {
322 self.channels.max(1)
323 }
324
325 pub fn sample_rate(&self) -> u32 {
326 self.sample_rate
327 }
328
329 pub fn reset(&mut self) -> io::Result<()> {
332 let (format, decoder, track_id, channels, sample_rate) = Self::open(&self.path)?;
333 self.format = format;
334 self.decoder = decoder;
335 self.track_id = track_id;
336 self.channels = channels;
337 self.sample_rate = sample_rate;
338 self.pending.clear();
339 Ok(())
340 }
341
342 pub fn next_chunk(&mut self, max_frames: usize) -> io::Result<Option<Vec<f32>>> {
345 let channels = self.channels();
346 let target = max_frames.saturating_mul(channels);
347 let mut out = std::mem::take(&mut self.pending);
348 if out.len() > target {
349 self.pending = out.split_off(target);
350 return Ok(Some(out));
351 }
352 while out.len() < target {
353 let packet = match self.format.next_packet() {
354 Ok(Some(packet)) => packet,
355 Ok(None) => break,
356 Err(SymphoniaError::IoError(e))
357 if e.kind() == std::io::ErrorKind::UnexpectedEof =>
358 {
359 break;
360 }
361 Err(e) => {
362 return Err(io::Error::other(format!(
363 "Symphonia read error for '{}': {e}",
364 self.path.display()
365 )));
366 }
367 };
368 if packet.track_id != self.track_id {
369 continue;
370 }
371 match self.decoder.decode(&packet) {
372 Ok(decoded) => {
373 let mut packet_samples = Vec::new();
374 decoded.copy_to_vec_interleaved(&mut packet_samples);
375 out.extend_from_slice(&packet_samples);
376 }
377 Err(SymphoniaError::DecodeError(_)) => continue,
379 Err(SymphoniaError::IoError(e))
380 if e.kind() == std::io::ErrorKind::UnexpectedEof =>
381 {
382 break;
383 }
384 Err(e) => {
385 return Err(io::Error::other(format!(
386 "Symphonia decode error for '{}': {e}",
387 self.path.display()
388 )));
389 }
390 }
391 }
392 if out.len() > target {
393 self.pending = out.split_off(target);
394 }
395 if out.is_empty() {
396 Ok(None)
397 } else {
398 Ok(Some(out))
399 }
400 }
401}
402
403fn decode_with_oxideav(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
408 let mut ctx = RuntimeContext::new();
409 oxideav_basic::register(&mut ctx);
410 oxideav_flac::register(&mut ctx);
411 oxideav_mp3::register(&mut ctx);
412 oxideav_ogg::register(&mut ctx);
413
414 let mut input: Box<dyn oxideav_core::ReadSeek> = Box::new(
415 std::fs::File::open(path)
416 .map_err(|e| io::Error::other(format!("Failed to open '{}': {e}", path.display())))?,
417 );
418 let ext_hint = path
419 .extension()
420 .and_then(|e| e.to_str())
421 .map(str::to_ascii_lowercase);
422 let container = ctx
423 .containers
424 .probe_input(&mut *input, ext_hint.as_deref())
425 .map_err(oxideav_err_to_io)?;
426 let mut demuxer = ctx
427 .containers
428 .open_demuxer(&container, input, &ctx.codecs)
429 .map_err(oxideav_err_to_io)?;
430
431 let (stream_index, params) = demuxer
432 .streams()
433 .iter()
434 .enumerate()
435 .map(|(i, s)| (i as u32, &s.params))
436 .find(|(_, p)| p.media_type == MediaType::Audio)
437 .ok_or_else(|| {
438 io::Error::other(format!("No usable audio track in '{}'", path.display()))
439 })?;
440 let params = params.clone();
441
442 let channels = params.channels.unwrap_or(1) as usize;
443 let sample_rate = params.sample_rate.unwrap_or(48_000);
444 let format = params.sample_format.unwrap_or(SampleFormat::S16);
447 let mut decoder = ctx
448 .codecs
449 .first_decoder(¶ms)
450 .map_err(oxideav_err_to_io)?;
451 demuxer.set_active_streams(std::slice::from_ref(&stream_index));
452
453 let mut samples = Vec::new();
454 loop {
455 match demuxer.next_packet() {
456 Ok(packet) => {
457 if packet.stream_index != stream_index {
458 continue;
459 }
460 decoder.send_packet(&packet).map_err(oxideav_err_to_io)?;
461 drain_decoder_frames(&mut *decoder, format, channels, &mut samples)?;
462 }
463 Err(oxideav_core::Error::Eof) => break,
464 Err(e) => return Err(oxideav_err_to_io(e)),
465 }
466 }
467 decoder.flush().map_err(oxideav_err_to_io)?;
468 drain_decoder_frames(&mut *decoder, format, channels, &mut samples)?;
469
470 if samples.is_empty() {
471 return Err(io::Error::other(format!(
472 "No samples decoded from '{}'",
473 path.display()
474 )));
475 }
476
477 Ok((samples, channels, sample_rate))
478}
479
480fn drain_decoder_frames(
483 decoder: &mut dyn oxideav_core::Decoder,
484 format: SampleFormat,
485 channels: usize,
486 out: &mut Vec<f32>,
487) -> io::Result<()> {
488 loop {
489 match decoder.receive_frame() {
490 Ok(Frame::Audio(frame)) => {
491 out.extend(unpack_audio_frame(&frame, format, channels)?);
492 }
493 Ok(_) => {}
494 Err(oxideav_core::Error::NeedMore) | Err(oxideav_core::Error::Eof) => return Ok(()),
495 Err(e) => return Err(oxideav_err_to_io(e)),
496 }
497 }
498}
499
500fn unpack_audio_frame(
504 frame: &AudioFrame,
505 format: SampleFormat,
506 channels: usize,
507) -> io::Result<Vec<f32>> {
508 let bytes_per_sample = format.bytes_per_sample();
509 if channels == 0 || frame.samples == 0 {
510 return Ok(Vec::new());
511 }
512 let total = frame
513 .samples
514 .checked_mul(channels as u32)
515 .ok_or_else(|| io::Error::other("OxideAV frame sample count overflow"))?
516 as usize;
517 let mut out = Vec::with_capacity(total);
518
519 let planar = frame.data.len() >= channels && frame.data.len() > 1;
524 if planar {
525 if frame.data.len() < channels {
526 return Err(io::Error::other(
527 "OxideAV planar frame has fewer planes than channels",
528 ));
529 }
530 for i in 0..frame.samples as usize {
531 for plane in frame.data.iter().take(channels) {
532 let start = i * bytes_per_sample;
533 let end = start + bytes_per_sample;
534 if end > plane.len() {
535 return Err(io::Error::other("OxideAV frame plane is truncated"));
536 }
537 out.push(sample_bytes_to_f32(&plane[start..end], format));
538 }
539 }
540 } else {
541 let plane = frame
542 .data
543 .first()
544 .ok_or_else(|| io::Error::other("OxideAV packed frame has no plane"))?;
545 if plane.len() < total * bytes_per_sample {
546 return Err(io::Error::other(format!(
547 "OxideAV frame plane is truncated: {} bytes for {total} samples \
548 at {bytes_per_sample} B/sample ({} declared per channel)",
549 plane.len(),
550 frame.samples
551 )));
552 }
553 for chunk in plane[..total * bytes_per_sample].chunks_exact(bytes_per_sample) {
554 out.push(sample_bytes_to_f32(chunk, format));
555 }
556 }
557
558 Ok(out)
559}
560
561fn sample_bytes_to_f32(bytes: &[u8], format: SampleFormat) -> f32 {
562 match format {
563 SampleFormat::U8 => (bytes[0] as f32 - 128.0) / 128.0,
564 SampleFormat::S16 => {
565 f32::from(i16::from_le_bytes([bytes[0], bytes[1]])) / f32::from(i16::MAX)
566 }
567 SampleFormat::S24 => {
568 let v = i32::from_le_bytes([
569 bytes[0],
570 bytes[1],
571 bytes[2],
572 if bytes[2] & 0x80 != 0 { 0xFF } else { 0 },
573 ]);
574 v as f32 / 8_388_607.0
575 }
576 SampleFormat::S32 => {
577 i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]) as f32 / i32::MAX as f32
578 }
579 SampleFormat::F32 => f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
580 _ => 0.0,
581 }
582}
583
584fn decode_with_symphonia(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
589 let file = std::fs::File::open(path)
590 .map_err(|e| io::Error::other(format!("Failed to open '{}': {e}", path.display())))?;
591 let mss = MediaSourceStream::new(Box::new(file), Default::default());
592
593 let mut hint = Hint::new();
594 if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
595 hint.with_extension(ext);
596 }
597
598 let format_opts = FormatOptions::default();
599 let metadata_opts = MetadataOptions::default();
600 let decoder_opts = AudioDecoderOptions::default();
601
602 let mut format: Box<dyn FormatReader> = symphonia::default::get_probe()
603 .probe(&hint, mss, format_opts, metadata_opts)
604 .map_err(|e| {
605 io::Error::other(format!(
606 "Symphonia failed to probe format for '{}': {e}",
607 path.display()
608 ))
609 })?;
610
611 let track = format
612 .default_track(TrackType::Audio)
613 .or_else(|| format.tracks().first())
614 .ok_or_else(|| {
615 io::Error::other(format!("No usable audio track in '{}'", path.display()))
616 })?;
617
618 let codec_params: &AudioCodecParameters = track
619 .codec_params
620 .as_ref()
621 .and_then(SymphoniaCodecParameters::audio)
622 .ok_or_else(|| {
623 io::Error::other(format!("No usable audio track in '{}'", path.display()))
624 })?;
625
626 let channels = codec_params
627 .channels
628 .as_ref()
629 .map(|c| c.count())
630 .unwrap_or(1);
631 let sample_rate = codec_params.sample_rate.unwrap_or(48_000);
632 let track_id = track.id;
633
634 let mut decoder = symphonia::default::get_codecs()
635 .make_audio_decoder(codec_params, &decoder_opts)
636 .map_err(|e| {
637 io::Error::other(format!(
638 "Symphonia failed to create decoder for '{}': {e}",
639 path.display()
640 ))
641 })?;
642
643 let mut samples = Vec::new();
644
645 loop {
646 let packet = match format.next_packet() {
647 Ok(Some(packet)) => packet,
648 Ok(None) => break,
649 Err(SymphoniaError::IoError(e)) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
650 break;
651 }
652 Err(e) => {
653 return Err(io::Error::other(format!(
654 "Symphonia read error for '{}': {e}",
655 path.display()
656 )));
657 }
658 };
659
660 if packet.track_id != track_id {
661 continue;
662 }
663
664 let decoded = decoder.decode(&packet).map_err(|e| {
665 io::Error::other(format!(
666 "Symphonia decode error for '{}': {e}",
667 path.display()
668 ))
669 })?;
670
671 let mut packet_samples = Vec::new();
672 decoded.copy_to_vec_interleaved(&mut packet_samples);
673 samples.extend_from_slice(&packet_samples);
674 }
675
676 if samples.is_empty() {
677 return Err(io::Error::other(format!(
678 "No samples decoded from '{}'",
679 path.display()
680 )));
681 }
682
683 Ok((samples, channels, sample_rate))
684}
685
686pub fn encode_audio_to_file(
698 path: &Path,
699 samples: &[f32],
700 channels: usize,
701 sample_rate: u32,
702 format: AudioEncodeFormat,
703 dither: AudioDither,
704) -> io::Result<()> {
705 let channels = channels.max(1);
706 if sample_rate == 0 {
707 return Err(io::Error::other("encode: sample_rate must be > 0"));
708 }
709 if channels > 8 {
710 return Err(io::Error::other(format!(
711 "encode: channel count {channels} exceeds the supported maximum of 8"
712 )));
713 }
714 if !samples.len().is_multiple_of(channels) {
715 return Err(io::Error::other(
716 "encode: sample slice length is not a multiple of channels",
717 ));
718 }
719
720 match format {
721 AudioEncodeFormat::Wav(depth) => {
722 encode_wav(path, samples, channels, sample_rate, depth, dither)
723 }
724 AudioEncodeFormat::Flac(bits) => {
725 encode_flac_to_file(path, samples, channels, sample_rate, bits, dither)
726 }
727 AudioEncodeFormat::OggFlac(bits) => {
728 encode_ogg_flac(path, samples, channels, sample_rate, bits, dither)
729 }
730 AudioEncodeFormat::Mp3 => encode_mp3(path, samples, channels, sample_rate, dither),
731 }
732}
733
734pub fn write_wav_f32(
736 path: &Path,
737 samples: &[f32],
738 channels: usize,
739 sample_rate: u32,
740) -> io::Result<()> {
741 encode_audio_to_file(
742 path,
743 samples,
744 channels,
745 sample_rate,
746 AudioEncodeFormat::Wav(WavBitDepth::Float32),
747 AudioDither::None,
748 )
749}
750
751pub fn write_flac(
753 path: &Path,
754 samples: &[f32],
755 channels: usize,
756 sample_rate: u32,
757 bits_per_sample: u16,
758) -> io::Result<()> {
759 encode_audio_to_file(
760 path,
761 samples,
762 channels,
763 sample_rate,
764 AudioEncodeFormat::Flac(bits_per_sample),
765 AudioDither::None,
766 )
767}
768
769fn encode_wav(
774 path: &Path,
775 samples: &[f32],
776 channels: usize,
777 sample_rate: u32,
778 depth: WavBitDepth,
779 dither: AudioDither,
780) -> io::Result<()> {
781 let (codec_id, sample_format) = match depth {
782 WavBitDepth::Int16 => ("pcm_s16le", SampleFormat::S16),
783 WavBitDepth::Int24 => ("pcm_s24le", SampleFormat::S24),
784 WavBitDepth::Int32 => ("pcm_s32le", SampleFormat::S32),
785 WavBitDepth::Float32 => ("pcm_f32le", SampleFormat::F32),
786 };
787 let bytes = pack_interleaved_samples(samples, sample_format, dither)?;
788
789 let mut ctx = RuntimeContext::new();
790 oxideav_basic::register(&mut ctx);
791
792 let stream = audio_stream_info(codec_id, channels, sample_rate, sample_format, None);
793 let file = std::fs::File::create(path)?;
794 let output: Box<dyn oxideav_core::WriteSeek> = Box::new(file);
795 let mut mux = ctx
796 .containers
797 .open_muxer("wav", output, std::slice::from_ref(&stream))
798 .map_err(oxideav_err_to_io)?;
799 mux.write_header().map_err(oxideav_err_to_io)?;
800 let packet = Packet::new(0, TimeBase::new(1, sample_rate as i64), bytes);
801 mux.write_packet(&packet).map_err(oxideav_err_to_io)?;
802 mux.write_trailer().map_err(oxideav_err_to_io)?;
803 Ok(())
804}
805
806fn encode_flac_to_file(
811 path: &Path,
812 samples: &[f32],
813 channels: usize,
814 sample_rate: u32,
815 bits_per_sample: u16,
816 dither: AudioDither,
817) -> io::Result<()> {
818 let (packets, output_params) =
819 encode_flac_packets(samples, channels, sample_rate, bits_per_sample, dither)?;
820
821 let mut ctx = RuntimeContext::new();
822 oxideav_flac::register(&mut ctx);
823
824 let stream = StreamInfo {
825 index: 0,
826 time_base: TimeBase::new(1, sample_rate as i64),
827 duration: None,
828 start_time: Some(0),
829 params: output_params,
830 };
831 let file = std::fs::File::create(path)?;
832 let output: Box<dyn oxideav_core::WriteSeek> = Box::new(file);
833 let mut mux = ctx
834 .containers
835 .open_muxer("flac", output, std::slice::from_ref(&stream))
836 .map_err(oxideav_err_to_io)?;
837 mux.write_header().map_err(oxideav_err_to_io)?;
838 for pkt in &packets {
839 mux.write_packet(pkt).map_err(oxideav_err_to_io)?;
840 }
841 mux.write_trailer().map_err(oxideav_err_to_io)?;
842 Ok(())
843}
844
845fn encode_flac_packets(
848 samples: &[f32],
849 channels: usize,
850 sample_rate: u32,
851 bits_per_sample: u16,
852 dither: AudioDither,
853) -> io::Result<(Vec<Packet>, CodecParameters)> {
854 let sample_format = flac_sample_format(bits_per_sample)?;
855 let bytes = pack_interleaved_samples(samples, sample_format, dither)?;
856
857 let mut ctx = RuntimeContext::new();
858 oxideav_flac::register(&mut ctx);
859
860 let params = audio_codec_params("flac", channels, sample_rate, sample_format, None);
861 let mut enc = ctx
862 .codecs
863 .first_encoder(¶ms)
864 .map_err(oxideav_err_to_io)?;
865
866 let frame = AudioFrame {
867 samples: (samples.len() / channels) as u32,
868 pts: Some(0),
869 data: vec![bytes],
870 };
871 enc.send_frame(&Frame::Audio(frame))
872 .map_err(oxideav_err_to_io)?;
873 enc.flush().map_err(oxideav_err_to_io)?;
874
875 let mut packets = Vec::new();
876 loop {
877 match enc.receive_packet() {
878 Ok(p) => packets.push(p),
879 Err(oxideav_core::Error::NeedMore) | Err(oxideav_core::Error::Eof) => break,
880 Err(e) => return Err(oxideav_err_to_io(e)),
881 }
882 }
883
884 Ok((packets, enc.output_params().clone()))
885}
886
887fn encode_ogg_flac(
888 path: &Path,
889 samples: &[f32],
890 channels: usize,
891 sample_rate: u32,
892 bits_per_sample: u16,
893 dither: AudioDither,
894) -> io::Result<()> {
895 let (packets, output_params) =
896 encode_flac_packets(samples, channels, sample_rate, bits_per_sample, dither)?;
897
898 let mut mapping = Vec::with_capacity(13);
901 mapping.push(0x7F);
902 mapping.extend_from_slice(b"FLAC");
903 mapping.push(0x01); mapping.push(0x00); mapping.extend_from_slice(&1u16.to_be_bytes());
907 mapping.extend_from_slice(b"fLaC");
908
909 let streaminfo = output_params.extradata;
910
911 let mut writer = oxideav_ogg::framing::PageWriter::new(0).with_page_target(4096);
912 writer.push_packet(&mapping, 0);
913 writer.flush_page();
914 writer.push_packet(&streaminfo, 0);
915 writer.flush_page();
916
917 for pkt in &packets {
918 let granule = pkt
919 .pts
920 .map(|pts| pts + pkt.duration.unwrap_or(0))
921 .unwrap_or(0);
922 writer.push_packet(&pkt.data, granule);
923 }
924
925 std::fs::write(path, writer.finish())?;
926 Ok(())
927}
928
929fn flac_sample_format(bits_per_sample: u16) -> io::Result<SampleFormat> {
930 match bits_per_sample {
931 8 => Ok(SampleFormat::U8),
932 16 => Ok(SampleFormat::S16),
933 24 => Ok(SampleFormat::S24),
934 32 => Ok(SampleFormat::S32),
935 _ => Err(io::Error::other(format!(
936 "FLAC bit depth {bits_per_sample} not supported (use 8, 16, 24 or 32)"
937 ))),
938 }
939}
940
941fn encode_mp3(
946 path: &Path,
947 samples: &[f32],
948 channels: usize,
949 sample_rate: u32,
950 dither: AudioDither,
951) -> io::Result<()> {
952 if channels > 2 {
953 return Err(io::Error::other(
954 "MP3 encode: only mono and stereo are supported",
955 ));
956 }
957 let bitrate = mp3_default_bitrate(sample_rate, channels);
958 let bytes = pack_interleaved_samples(samples, SampleFormat::S16, dither)?;
959
960 let mut ctx = RuntimeContext::new();
961 oxideav_mp3::register(&mut ctx);
962
963 let params = audio_codec_params(
964 "mp3",
965 channels,
966 sample_rate,
967 SampleFormat::S16,
968 Some(bitrate as u64),
969 );
970 let mut enc = ctx
971 .codecs
972 .first_encoder(¶ms)
973 .map_err(oxideav_err_to_io)?;
974
975 let frame = AudioFrame {
976 samples: (samples.len() / channels) as u32,
977 pts: Some(0),
978 data: vec![bytes],
979 };
980 enc.send_frame(&Frame::Audio(frame))
981 .map_err(oxideav_err_to_io)?;
982 enc.flush().map_err(oxideav_err_to_io)?;
983
984 let mut file = std::fs::File::create(path)?;
985 loop {
986 match enc.receive_packet() {
987 Ok(pkt) => file.write_all(&pkt.data)?,
988 Err(oxideav_core::Error::NeedMore) | Err(oxideav_core::Error::Eof) => break,
989 Err(e) => return Err(oxideav_err_to_io(e)),
990 }
991 }
992 Ok(())
993}
994
995fn mp3_default_bitrate(sample_rate: u32, channels: usize) -> u32 {
996 if sample_rate >= 32_000 {
998 if channels >= 2 { 192_000 } else { 128_000 }
999 } else if sample_rate >= 16_000 {
1000 if channels >= 2 { 96_000 } else { 64_000 }
1002 } else {
1003 if channels >= 2 { 48_000 } else { 32_000 }
1005 }
1006}
1007
1008fn audio_codec_params(
1013 codec_id: &str,
1014 channels: usize,
1015 sample_rate: u32,
1016 sample_format: SampleFormat,
1017 bit_rate: Option<u64>,
1018) -> CodecParameters {
1019 let mut params = CodecParameters::audio(CodecId::new(codec_id));
1020 params.media_type = MediaType::Audio;
1021 params.channels = Some(channels as u16);
1022 params.sample_rate = Some(sample_rate);
1023 params.sample_format = Some(sample_format);
1024 if let Some(br) = bit_rate {
1025 params.bit_rate = Some(br);
1026 }
1027 params
1028}
1029
1030fn audio_stream_info(
1031 codec_id: &str,
1032 channels: usize,
1033 sample_rate: u32,
1034 sample_format: SampleFormat,
1035 bit_rate: Option<u64>,
1036) -> StreamInfo {
1037 let params = audio_codec_params(codec_id, channels, sample_rate, sample_format, bit_rate);
1038 StreamInfo {
1039 index: 0,
1040 time_base: TimeBase::new(1, sample_rate as i64),
1041 duration: None,
1042 start_time: Some(0),
1043 params,
1044 }
1045}
1046
1047fn pack_interleaved_samples(
1048 samples: &[f32],
1049 format: SampleFormat,
1050 dither: AudioDither,
1051) -> io::Result<Vec<u8>> {
1052 let bytes_per_sample = format.bytes_per_sample();
1053 let mut out = Vec::with_capacity(samples.len().saturating_mul(bytes_per_sample));
1054 let mut rng = DitherRng::new(0x1234_5678_9abc_defe);
1055
1056 for &sample in samples {
1057 let s = sample.clamp(-1.0, 1.0);
1058 match format {
1059 SampleFormat::U8 => {
1060 let v = ((s + 1.0) * 127.5 + dither_offset(&mut rng, dither)).round() as u8;
1061 out.push(v);
1062 }
1063 SampleFormat::S16 => {
1064 let scale = i16::MAX as f32;
1065 let q = quantize_with_dither(s, scale, &mut rng, dither)
1066 .round()
1067 .clamp(i16::MIN as f32, i16::MAX as f32) as i16;
1068 out.extend_from_slice(&q.to_le_bytes());
1069 }
1070 SampleFormat::S24 => {
1071 let scale = 8_388_607.0;
1072 let q = quantize_with_dither(s, scale, &mut rng, dither)
1073 .round()
1074 .clamp(-8_388_608.0, 8_388_607.0) as i32;
1075 let b = q.to_le_bytes();
1076 out.extend_from_slice(&b[..3]);
1077 }
1078 SampleFormat::S32 => {
1079 let scale = i32::MAX as f32;
1080 let q = quantize_with_dither(s, scale, &mut rng, dither)
1081 .round()
1082 .clamp(i32::MIN as f32, i32::MAX as f32) as i32;
1083 out.extend_from_slice(&q.to_le_bytes());
1084 }
1085 SampleFormat::F32 => {
1086 out.extend_from_slice(&s.to_le_bytes());
1087 }
1088 _ => {
1089 return Err(io::Error::other(format!(
1090 "unsupported sample format {format:?}"
1091 )));
1092 }
1093 }
1094 }
1095 Ok(out)
1096}
1097
1098fn quantize_with_dither(sample: f32, scale: f32, rng: &mut DitherRng, dither: AudioDither) -> f32 {
1099 let d = dither_offset(rng, dither);
1100 (sample + d / scale).clamp(-1.0, 1.0) * scale
1101}
1102
1103fn dither_offset(rng: &mut DitherRng, dither: AudioDither) -> f32 {
1104 match dither {
1105 AudioDither::None => 0.0,
1106 AudioDither::Rectangular => rng.uniform_half(),
1107 AudioDither::Triangular => rng.uniform_half() + rng.uniform_half(),
1108 }
1109}
1110
1111fn oxideav_err_to_io(e: oxideav_core::Error) -> io::Error {
1112 io::Error::other(format!("OxideAV error: {e}"))
1113}
1114
1115struct DitherRng {
1117 state: u64,
1118}
1119
1120impl DitherRng {
1121 fn new(seed: u64) -> Self {
1122 Self { state: seed.max(1) }
1123 }
1124
1125 fn next_u64(&mut self) -> u64 {
1126 self.state ^= self.state >> 12;
1128 self.state ^= self.state << 25;
1129 self.state ^= self.state >> 27;
1130 self.state.wrapping_mul(0x2545_f491_4f6c_dd1d)
1131 }
1132
1133 fn uniform_half(&mut self) -> f32 {
1135 let u = self.next_u64() >> 32;
1136 (u as f32 / 4_294_967_296.0) - 0.5
1137 }
1138}
1139
1140#[cfg(test)]
1141mod tests {
1142 use super::*;
1143
1144 #[test]
1145 fn decode_stereo_wav_returns_interleaved_samples() {
1146 let path =
1147 std::env::temp_dir().join(format!("maolan_stereo_decode_{}.wav", std::process::id()));
1148 write_test_wav_f32(
1149 &path,
1150 &[
1151 0.10, 0.60, 0.20, 0.70, 0.30, 0.80, 0.40, 0.90,
1155 ],
1156 2,
1157 48_000,
1158 )
1159 .expect("write test wav");
1160
1161 let (samples, channels, sample_rate) =
1162 decode_audio_to_f32_interleaved_sync(&path).expect("decode test wav");
1163 let _ = std::fs::remove_file(&path);
1164
1165 assert_eq!(channels, 2);
1166 assert_eq!(sample_rate, 48_000);
1167 assert_eq!(samples.len(), 8);
1168 for (actual, expected) in samples
1169 .iter()
1170 .zip([0.10, 0.60, 0.20, 0.70, 0.30, 0.80, 0.40, 0.90])
1171 {
1172 assert!((actual - expected).abs() < 1.0e-6);
1173 }
1174 }
1175
1176 #[test]
1177 fn oxideav_fallback_decodes_flac_and_mp3() {
1178 let sample_rate = 44_100u32;
1179 let channels = 2usize;
1180 let frames = sample_rate as usize / 10; let source: Vec<f32> = (0..frames)
1182 .flat_map(|i| {
1183 let s = 0.5
1184 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin();
1185 [s, s]
1186 })
1187 .collect();
1188
1189 for (ext, format) in [
1190 ("flac", AudioEncodeFormat::Flac(16)),
1191 ("mp3", AudioEncodeFormat::Mp3),
1192 ] {
1193 let path = std::env::temp_dir().join(format!(
1194 "maolan_oxideav_fallback_{}_{}.{}",
1195 ext,
1196 std::process::id(),
1197 ext
1198 ));
1199 encode_audio_to_file(
1200 &path,
1201 &source,
1202 channels,
1203 sample_rate,
1204 format,
1205 AudioDither::None,
1206 )
1207 .expect("encode test file");
1208
1209 let (decoded, out_channels, out_rate) =
1211 decode_with_oxideav(&path).expect("oxideav decode");
1212 let _ = std::fs::remove_file(&path);
1213
1214 assert_eq!(out_channels, channels);
1215 assert_eq!(out_rate, sample_rate);
1216 assert!(!decoded.is_empty());
1217 assert!(decoded.iter().all(|s| s.is_finite() && s.abs() <= 1.0));
1218 let peak = decoded.iter().fold(0.0f32, |a, &b| a.max(b.abs()));
1220 assert!(peak > 0.2, "{ext}: decoded signal too quiet (peak {peak})");
1221 }
1222 }
1223
1224 #[test]
1225 fn probe_audio_file_reports_wav_metadata() {
1226 let sample_rate = 48_000u32;
1227 let channels = 2usize;
1228 let frames = 1_000usize;
1229 let source: Vec<f32> = (0..frames)
1230 .flat_map(|i| {
1231 let s = 0.5
1232 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin();
1233 [s, s]
1234 })
1235 .collect();
1236 let path =
1237 std::env::temp_dir().join(format!("maolan_probe_wav_{}.wav", std::process::id()));
1238 write_wav_f32(&path, &source, channels, sample_rate).expect("write test wav");
1239
1240 let info = probe_audio_file(&path).expect("probe wav");
1241 let _ = std::fs::remove_file(&path);
1242
1243 assert_eq!(info.sample_rate, sample_rate);
1244 assert_eq!(info.channels, channels);
1245 let probed_frames = info.frames.expect("wav probe should report frames");
1246 assert!(
1247 (probed_frames as i64 - frames as i64).abs() <= 1,
1248 "probed frames {probed_frames} != {frames}"
1249 );
1250 }
1251
1252 #[test]
1253 fn probe_audio_file_reports_flac_metadata() {
1254 let sample_rate = 44_100u32;
1255 let channels = 1usize;
1256 let frames = 44_100usize;
1257 let source: Vec<f32> = (0..frames)
1258 .map(|i| {
1259 0.5 * (2.0 * std::f32::consts::PI * 440.0 * i as f32 / sample_rate as f32).sin()
1260 })
1261 .collect();
1262 let path =
1263 std::env::temp_dir().join(format!("maolan_probe_flac_{}.flac", std::process::id()));
1264 encode_audio_to_file(
1265 &path,
1266 &source,
1267 channels,
1268 sample_rate,
1269 AudioEncodeFormat::Flac(16),
1270 AudioDither::None,
1271 )
1272 .expect("encode test flac");
1273
1274 let info = probe_audio_file(&path).expect("probe flac");
1275 let _ = std::fs::remove_file(&path);
1276
1277 assert_eq!(info.sample_rate, sample_rate);
1278 assert_eq!(info.channels, channels);
1279 let probed_frames = info.frames.expect("flac probe should report frames");
1280 assert!(
1281 (probed_frames as i64 - frames as i64).abs() <= 1,
1282 "probed frames {probed_frames} != {frames}"
1283 );
1284 }
1285
1286 #[test]
1287 fn streaming_decoder_yields_incremental_flac_chunks() {
1288 let sample_rate = 44_100u32;
1289 let channels = 2usize;
1290 let frames = 20_000usize;
1291 let source: Vec<f32> = (0..frames)
1292 .flat_map(|i| {
1293 let s = 0.4
1294 * (2.0 * std::f32::consts::PI * 330.0 * i as f32 / sample_rate as f32).sin();
1295 [s, -s]
1296 })
1297 .collect();
1298 let path = std::env::temp_dir().join(format!(
1299 "maolan_streaming_decoder_{}.flac",
1300 std::process::id()
1301 ));
1302 encode_audio_to_file(
1303 &path,
1304 &source,
1305 channels,
1306 sample_rate,
1307 AudioEncodeFormat::Flac(16),
1308 AudioDither::None,
1309 )
1310 .expect("encode test flac");
1311
1312 let mut decoder = StreamingDecoder::new(&path).expect("open streaming decoder");
1313 assert_eq!(decoder.channels(), channels);
1314 assert_eq!(decoder.sample_rate(), sample_rate);
1315
1316 let mut total_frames = 0usize;
1317 let mut chunks = 0usize;
1318 loop {
1319 match decoder.next_chunk(4096) {
1320 Ok(Some(chunk)) => {
1321 chunks += 1;
1322 assert!(chunk.len() % channels == 0);
1323 total_frames += chunk.len() / channels;
1324 assert!(chunk.iter().all(|s| s.is_finite()));
1325 }
1326 Ok(None) => break,
1327 Err(e) => panic!("streaming decode failed: {e}"),
1328 }
1329 }
1330 let _ = std::fs::remove_file(&path);
1331
1332 assert!(chunks > 1, "expected multiple chunks, got {chunks}");
1333 assert!(
1334 (total_frames as i64 - frames as i64).abs() <= 8_192,
1335 "decoded frames {total_frames} far from {frames}"
1336 );
1337 }
1338
1339 fn write_test_wav_f32(
1340 path: &Path,
1341 samples: &[f32],
1342 channels: usize,
1343 sample_rate: u32,
1344 ) -> io::Result<()> {
1345 let bytes_per_sample = 4usize;
1346 let block_align = (channels * bytes_per_sample) as u16;
1347 let byte_rate = sample_rate * u32::from(block_align);
1348 let data_size = samples.len() * bytes_per_sample;
1349 let riff_size = 36 + data_size as u32;
1350
1351 let mut file = std::fs::File::create(path)?;
1352 file.write_all(b"RIFF")?;
1353 file.write_all(&riff_size.to_le_bytes())?;
1354 file.write_all(b"WAVE")?;
1355 file.write_all(b"fmt ")?;
1356 file.write_all(&16u32.to_le_bytes())?;
1357 file.write_all(&3u16.to_le_bytes())?;
1358 file.write_all(&(channels as u16).to_le_bytes())?;
1359 file.write_all(&sample_rate.to_le_bytes())?;
1360 file.write_all(&byte_rate.to_le_bytes())?;
1361 file.write_all(&block_align.to_le_bytes())?;
1362 file.write_all(&32u16.to_le_bytes())?;
1363 file.write_all(b"data")?;
1364 file.write_all(&(data_size as u32).to_le_bytes())?;
1365 for sample in samples {
1366 file.write_all(&sample.to_le_bytes())?;
1367 }
1368 Ok(())
1369 }
1370}