Skip to main content

maolan_engine/
audio_codec.rs

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