Skip to main content

maolan_engine/
audio_codec.rs

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/// Export format selector for [`encode_audio_to_file`].
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum AudioEncodeFormat {
19    /// Microsoft RIFF/WAVE, integer or float PCM.
20    Wav(WavBitDepth),
21    /// Native FLAC (`*.flac`). The `u16` is the desired bit depth
22    /// (16, 24 or 32).
23    Flac(u16),
24    /// Ogg-encapsulated FLAC (`*.ogg`). The `u16` is the desired bit
25    /// depth (16, 24 or 32).
26    OggFlac(u16),
27    /// MPEG-1/2/2.5 Layer III (`*.mp3`). Uses a sensible CBR bitrate
28    /// chosen from the standard Layer III ladder based on sample rate
29    /// and channel count.
30    Mp3,
31}
32
33/// WAV PCM bit-depth / sample-format choices.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum WavBitDepth {
36    Int16,
37    Int24,
38    Int32,
39    Float32,
40}
41
42/// Dither mode applied when quantising floating-point samples to an
43/// integer target format.
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
45pub enum AudioDither {
46    #[default]
47    None,
48    Rectangular,
49    Triangular,
50}
51
52/// Decode an audio file to interleaved `f32` samples.
53///
54/// The format is auto-detected by Symphonia for `.wav`, `.flac`, `.mp3`,
55/// `.ogg`/`.vorbis`, `.m4a`/`.aac`/`.alac`, and friends.
56/// Returns `(samples, channels, sample_rate)`.
57/// All decode paths emit samples in the range `[-1.0, 1.0]`.
58pub fn decode_audio_to_f32_interleaved_sync(path: &Path) -> io::Result<(Vec<f32>, usize, u32)> {
59    decode_with_symphonia(path)
60}
61
62/// Decode a WAV file preferentially, falling back to the general decoder.
63///
64/// This used to short-circuit `.wav` inputs to a dedicated path. Symphonia
65/// handles WAV natively, so this now simply delegates to the unified decoder.
66pub 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
72// ---------------------------------------------------------------------------
73// Symphonia decode (WAV, FLAC, MP3, Vorbis, AAC, ALAC, ...)
74// ---------------------------------------------------------------------------
75
76fn 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
169// ---------------------------------------------------------------------------
170// Encode entry point
171// ---------------------------------------------------------------------------
172
173/// Encode interleaved `f32` samples to a file using OxideAV.
174///
175/// `samples` must be interleaved (`ch0 ch1 ... chN ...`).
176/// `channels` is clamped to at least 1. `sample_rate` must be non-zero.
177/// Integer formats are quantised from the `[-1.0, 1.0]` float range; for
178/// WAV and FLAC the requested bit depth is honoured, while MP3 always
179/// uses 16-bit PCM internally.
180pub 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
217/// Backwards-compatible WAV writer: 32-bit float PCM.
218pub 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
234/// Backwards-compatible native FLAC writer.
235pub 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
252// ---------------------------------------------------------------------------
253// WAV
254// ---------------------------------------------------------------------------
255
256fn 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
289// ---------------------------------------------------------------------------
290// FLAC (native and Ogg)
291// ---------------------------------------------------------------------------
292
293fn 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
328/// Returns the encoded FLAC frame packets and the finalised output
329/// parameters (including the STREAMINFO extradata).
330fn 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(&params)
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    // Build the FLAC-in-Ogg mapping header packet:
382    // 0x7F "FLAC" major minor header_packets_be "fLaC"
383    let mut mapping = Vec::with_capacity(13);
384    mapping.push(0x7F);
385    mapping.extend_from_slice(b"FLAC");
386    mapping.push(0x01); // mapping major version
387    mapping.push(0x00); // mapping minor version
388    // One header packet follows the mapping header: the STREAMINFO block.
389    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
424// ---------------------------------------------------------------------------
425// MP3
426// ---------------------------------------------------------------------------
427
428fn 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(&params)
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    // MPEG-1 (32/44.1/48 kHz) ladder
480    if sample_rate >= 32_000 {
481        if channels >= 2 { 192_000 } else { 128_000 }
482    } else if sample_rate >= 16_000 {
483        // MPEG-2 LSF (16/22.05/24 kHz) ladder
484        if channels >= 2 { 96_000 } else { 64_000 }
485    } else {
486        // MPEG-2.5 (8/11.025/12 kHz) ladder
487        if channels >= 2 { 48_000 } else { 32_000 }
488    }
489}
490
491// ---------------------------------------------------------------------------
492// Helpers
493// ---------------------------------------------------------------------------
494
495fn 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
598/// Tiny deterministic PRNG used for export dither.
599struct 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        // xorshift64*
610        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    /// Uniform random value in [-0.5, 0.5).
617    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, //
635                0.20, 0.70, //
636                0.30, 0.80, //
637                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}