Skip to main content

maolan_engine/
audio_codec.rs

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/// 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 the OxideAV demuxers/decoders:
55/// `.wav`/PCM, `.flac`, `.mp3`, and Ogg-encapsulated FLAC (`.ogg`).
56/// Files OxideAV cannot probe or decode fall back to Symphonia
57/// (`.ogg`/Vorbis, `.m4a`/`.aac`/`.alac`, and friends).
58/// Returns `(samples, channels, sample_rate)`.
59/// All decode paths emit samples in the range `[-1.0, 1.0]`.
60pub 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
73/// Decode a WAV file preferentially, falling back to the general decoder.
74///
75/// This used to short-circuit `.wav` inputs to a dedicated path. The
76/// unified OxideAV decoder handles WAV natively, so this now simply
77/// delegates to it.
78pub 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// ---------------------------------------------------------------------------
85// Metadata probe + incremental streaming decode (Symphonia)
86// ---------------------------------------------------------------------------
87
88/// Fast metadata probe result for an audio file.
89#[derive(Debug, Clone, Copy, PartialEq, Eq)]
90pub struct AudioFileInfo {
91    pub sample_rate: u32,
92    pub channels: usize,
93    /// Approximate total frame count, when the container reports a duration.
94    pub frames: Option<u64>,
95}
96
97/// Probe sample rate, channel count and approximate frame count without
98/// decoding. Works for every format Symphonia can probe (wav, flac, mp3,
99/// ogg/vorbis, aac/alac in m4a/isomp4, ...).
100pub 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/// Metadata (tags) read from an audio file, all fields optional.
145#[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
155/// Read metadata tags from an audio file without decoding audio.
156///
157/// Container tags (e.g. Vorbis comments, ID3v2) are preferred; track-level
158/// tags (e.g. MP4 metadata) are used as a fallback.
159pub 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    // Media-level tags first, then per-track tags as a fallback.
183    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            // Fall back to the raw key for tags Symphonia doesn't map.
203            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
250/// Parts of a probed + opened Symphonia audio stream: the format reader, the
251/// audio decoder, the audio track id, channel count and sample rate.
252type SymphoniaStreamParts = (
253    Box<dyn FormatReader>,
254    Box<dyn AudioDecoder>,
255    u32,
256    usize,
257    u32,
258);
259
260/// Incremental audio decoder: probes once, then yields decoded interleaved
261/// `f32` chunks on demand. Used by the streaming clip producer thread so the
262/// audio thread never decodes whole files.
263pub 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 decoded samples carried between chunk boundaries.
271    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    /// Restart decoding from the beginning of the file. Used for seeks: the
330    /// caller decodes and discards frames until the target position.
331    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    /// Decode and return up to `max_frames` interleaved frames.
343    /// Returns `Ok(None)` at end of stream.
344    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                // Skip individual broken packets; fail only on IO-level errors.
378                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
403// ---------------------------------------------------------------------------
404// OxideAV decode (WAV/PCM, FLAC, MP3, Ogg-FLAC)
405// ---------------------------------------------------------------------------
406
407fn 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    // The MP3 demuxer leaves `sample_format` unset; its decoder always
445    // emits packed S16. Every other OxideAV decoder declares its format.
446    let format = params.sample_format.unwrap_or(SampleFormat::S16);
447    let mut decoder = ctx
448        .codecs
449        .first_decoder(&params)
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
480/// Pull every decoded frame currently pending on `decoder` and append it
481/// to `out` as interleaved `f32`.
482fn 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
500/// Convert one `AudioFrame` to interleaved `f32` samples in `[-1.0, 1.0]`.
501/// Packed formats carry a single interleaved plane; planar formats carry
502/// one plane per channel.
503fn 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    // Layout is inferred from the actual planes: one plane holding
520    // `total` samples means packed/interleaved; one plane per channel
521    // (as the MP3 decoder emits, despite its packed S16 tag) means
522    // planar.
523    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
584// ---------------------------------------------------------------------------
585// Symphonia decode fallback (Ogg Vorbis, AAC, ALAC, MP4/M4A, ...)
586// ---------------------------------------------------------------------------
587
588fn 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
686// ---------------------------------------------------------------------------
687// Encode entry point
688// ---------------------------------------------------------------------------
689
690/// Encode interleaved `f32` samples to a file using OxideAV.
691///
692/// `samples` must be interleaved (`ch0 ch1 ... chN ...`).
693/// `channels` is clamped to at least 1. `sample_rate` must be non-zero.
694/// Integer formats are quantised from the `[-1.0, 1.0]` float range; for
695/// WAV and FLAC the requested bit depth is honoured, while MP3 always
696/// uses 16-bit PCM internally.
697pub 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
734/// Backwards-compatible WAV writer: 32-bit float PCM.
735pub 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
751/// Backwards-compatible native FLAC writer.
752pub 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
769// ---------------------------------------------------------------------------
770// WAV
771// ---------------------------------------------------------------------------
772
773fn 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
806// ---------------------------------------------------------------------------
807// FLAC (native and Ogg)
808// ---------------------------------------------------------------------------
809
810fn 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
845/// Returns the encoded FLAC frame packets and the finalised output
846/// parameters (including the STREAMINFO extradata).
847fn 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(&params)
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    // Build the FLAC-in-Ogg mapping header packet:
899    // 0x7F "FLAC" major minor header_packets_be "fLaC"
900    let mut mapping = Vec::with_capacity(13);
901    mapping.push(0x7F);
902    mapping.extend_from_slice(b"FLAC");
903    mapping.push(0x01); // mapping major version
904    mapping.push(0x00); // mapping minor version
905    // One header packet follows the mapping header: the STREAMINFO block.
906    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
941// ---------------------------------------------------------------------------
942// MP3
943// ---------------------------------------------------------------------------
944
945fn 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(&params)
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    // MPEG-1 (32/44.1/48 kHz) ladder
997    if sample_rate >= 32_000 {
998        if channels >= 2 { 192_000 } else { 128_000 }
999    } else if sample_rate >= 16_000 {
1000        // MPEG-2 LSF (16/22.05/24 kHz) ladder
1001        if channels >= 2 { 96_000 } else { 64_000 }
1002    } else {
1003        // MPEG-2.5 (8/11.025/12 kHz) ladder
1004        if channels >= 2 { 48_000 } else { 32_000 }
1005    }
1006}
1007
1008// ---------------------------------------------------------------------------
1009// Helpers
1010// ---------------------------------------------------------------------------
1011
1012fn 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
1115/// Tiny deterministic PRNG used for export dither.
1116struct 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        // xorshift64*
1127        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    /// Uniform random value in [-0.5, 0.5).
1134    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, //
1152                0.20, 0.70, //
1153                0.30, 0.80, //
1154                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; // 100 ms
1181        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            // Bypass Symphonia to exercise the OxideAV fallback directly.
1210            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            // MP3 adds codec delay/padding; just require audible signal.
1219            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}