audio_samples_io 0.3.1

A Rust library for audio input and output operations.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
//! Streaming FLAC file writer for memory-efficient incremental encoding.
//!
//! This module provides [`StreamedFlacWriter`], a streaming writer that writes the FLAC
//! marker and a placeholder STREAMINFO block on construction, then encodes audio one
//! block at a time as frames are supplied. This enables writing large files without
//! buffering the whole stream in memory.
//!
//! # Why streaming FLAC is different from streaming WAV
//!
//! Unlike WAV, FLAC is a *block-based* codec: samples are grouped into fixed-size blocks
//! and each block is entropy-coded into a self-contained frame. The writer therefore
//! buffers incoming samples until a full block (`block_size`) is available, encodes that
//! block with the same routine used by the bulk [`write_flac`](super::write_flac) path,
//! and flushes the encoded frame. Any trailing partial block is encoded on
//! [`finalize`](AudioStreamWriter::finalize).
//!
//! The total sample count and block sizes in STREAMINFO are unknown up front, so the
//! writer requires a [`WriteSeek`] destination and back-patches the STREAMINFO block on
//! finalize. The MD5 signature is left zeroed (the "unknown" sentinel), exactly as the
//! bulk encoder does.

use std::io::{SeekFrom, Write};

use audio_samples::{AudioSamples, SampleType, traits::StandardSample};

use super::{
    CompressionLevel, constants::FLAC_MARKER, flac_file::audio_to_planar_i32, frame::encode_frame_from_channels,
    metadata::StreamInfo,
};
use crate::{
    WriteSeek,
    error::{AudioIOError, AudioIOResult},
    traits::{AudioStreamWrite, AudioStreamWriter},
    types::ValidatedSampleType,
};

/// Encoder parameters derived once from a [`CompressionLevel`], reused for every frame.
#[derive(Debug, Clone, Copy)]
struct FlacEncodeParams {
    max_lpc_order: usize,
    qlp_precision: u8,
    min_partition_order: u8,
    max_partition_order: u8,
    try_mid_side: bool,
    exhaustive_rice: bool,
}

impl FlacEncodeParams {
    const fn from_level(level: CompressionLevel) -> Self {
        let (min_partition_order, max_partition_order) = level.rice_partition_order_range();
        Self {
            max_lpc_order: level.max_lpc_order() as usize,
            qlp_precision: level.qlp_precision(),
            min_partition_order,
            max_partition_order,
            try_mid_side: level.try_mid_side(),
            exhaustive_rice: level.exhaustive_rice_search(),
        }
    }
}

/// A streaming FLAC writer that encodes audio incrementally, block by block.
///
/// Unlike [`write_flac`](super::write_flac), which requires the whole signal in memory,
/// `StreamedFlacWriter` writes the FLAC marker and a placeholder STREAMINFO on
/// construction and encodes frames as they are provided. This suits:
/// - Writing files larger than available memory
/// - Real-time recording / capture pipelines
/// - Streaming to seekable network destinations
///
/// # Bit depth
///
/// The output bit depth is fixed at construction from the configured sample type, matching
/// [`write_flac`]: 16-bit for `i16`, 24-bit for every other supported type. Each call to
/// [`write_frames`](AudioStreamWrite::write_frames) must supply samples that map to the
/// same bit depth.
///
/// # Finalization
///
/// Call [`finalize`](AudioStreamWriter::finalize) when done to encode the trailing partial
/// block and back-patch STREAMINFO with the final sample count. If the writer is dropped
/// without an explicit `finalize`, it finalizes on a best-effort basis (errors are
/// swallowed, since [`Drop`] cannot report them); prefer calling `finalize` yourself so
/// I/O errors surface.
#[derive(Debug)]
pub struct StreamedFlacWriter<W>
where
    W: WriteSeek,
{
    /// The underlying seekable writer.
    writer: W,
    /// Number of channels.
    channels: u16,
    /// Sample rate in Hz.
    sample_rate: u32,
    /// FLAC output bit depth (16 or 24).
    bits_per_sample: u8,
    /// Encoding block size (samples per channel per full frame).
    block_size: u32,
    /// Encoder parameters derived from the compression level.
    params: FlacEncodeParams,
    /// Per-channel buffer of i32 samples not yet emitted as a full block.
    accum: Vec<Vec<i32>>,
    /// Next frame number (FLAC fixed-blocksize frame counter).
    frame_number: u64,
    /// Total frames (samples per channel) accepted so far.
    frames_written: usize,
    /// File offset of the 34-byte STREAMINFO body (for back-patching on finalize).
    streaminfo_offset: u64,
    /// Whether finalize() has run.
    finalized: bool,
}

impl<W> StreamedFlacWriter<W>
where
    W: WriteSeek,
{
    /// Create a streaming FLAC writer for 16-bit output.
    pub fn new_i16(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<Self> {
        Self::new(
            writer,
            channels,
            sample_rate,
            ValidatedSampleType::I16,
            CompressionLevel::default(),
        )
    }

    /// Create a streaming FLAC writer for 24-bit output from 24-bit PCM input.
    pub fn new_i24(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<Self> {
        Self::new(
            writer,
            channels,
            sample_rate,
            ValidatedSampleType::I24,
            CompressionLevel::default(),
        )
    }

    /// Create a streaming FLAC writer for 24-bit output from 32-bit PCM input.
    pub fn new_i32(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<Self> {
        Self::new(
            writer,
            channels,
            sample_rate,
            ValidatedSampleType::I32,
            CompressionLevel::default(),
        )
    }

    /// Create a streaming FLAC writer for 24-bit output from 32-bit float input.
    pub fn new_f32(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<Self> {
        Self::new(
            writer,
            channels,
            sample_rate,
            ValidatedSampleType::F32,
            CompressionLevel::default(),
        )
    }

    /// Create a streaming FLAC writer for 24-bit output from 64-bit float input.
    pub fn new_f64(writer: W, channels: u16, sample_rate: u32) -> AudioIOResult<Self> {
        Self::new(
            writer,
            channels,
            sample_rate,
            ValidatedSampleType::F64,
            CompressionLevel::default(),
        )
    }

    /// Create a streaming FLAC writer with an explicit sample type and compression level.
    ///
    /// The bit depth is derived from `sample_type` exactly as [`write_flac`](super::write_flac):
    /// 16-bit for [`ValidatedSampleType::I16`], 24-bit otherwise.
    pub fn new(
        mut writer: W,
        channels: u16,
        sample_rate: u32,
        sample_type: ValidatedSampleType,
        level: CompressionLevel,
    ) -> AudioIOResult<Self> {
        if channels == 0 || channels > 8 {
            return Err(AudioIOError::corrupted_data_simple(
                "Invalid channel count for FLAC",
                format!("FLAC supports 1-8 channels, got {channels}"),
            ));
        }

        let bits_per_sample = flac_bits_for(sample_type);
        let block_size = level.block_size();

        // Write the FLAC marker and a placeholder STREAMINFO block. total_samples and the
        // final block sizes are back-patched in finalize() once they are known.
        writer.write_all(&FLAC_MARKER)?;
        let stream_info = StreamInfo {
            min_block_size: block_size as u16,
            max_block_size: block_size as u16,
            min_frame_size: 0,
            max_frame_size: 0,
            sample_rate,
            channels: channels as u8,
            bits_per_sample,
            total_samples: 0,
            md5_signature: [0; 16],
        };
        let streaminfo_bytes = stream_info.to_bytes();
        writer.write_all(&[0x80])?; // Last metadata block, type 0 (STREAMINFO)
        writer.write_all(&[(streaminfo_bytes.len() >> 16) as u8])?;
        writer.write_all(&[(streaminfo_bytes.len() >> 8) as u8])?;
        writer.write_all(&[streaminfo_bytes.len() as u8])?;
        let streaminfo_offset = writer.stream_position()?;
        writer.write_all(&streaminfo_bytes)?;

        let accum = (0..channels as usize).map(|_| Vec::new()).collect();

        Ok(Self {
            writer,
            channels,
            sample_rate,
            bits_per_sample,
            block_size,
            params: FlacEncodeParams::from_level(level),
            accum,
            frame_number: 0,
            frames_written: 0,
            streaminfo_offset,
            finalized: false,
        })
    }
}

/// FLAC output bit depth for a configured sample type, matching `write_flac`'s rule.
const fn flac_bits_for(sample_type: ValidatedSampleType) -> u8 {
    match sample_type {
        ValidatedSampleType::I16 => 16,
        _ => 24,
    }
}

/// Encode `accum[..][start..start + len]` as one FLAC frame and write it to `writer`.
///
/// Takes `writer` and `accum` as separate borrows so the caller can pass disjoint fields
/// of `self`.
fn encode_and_write_block<W: Write>(
    writer: &mut W,
    accum: &[Vec<i32>],
    start: usize,
    len: usize,
    bits_per_sample: u8,
    sample_rate: u32,
    frame_number: u64,
    params: &FlacEncodeParams,
) -> AudioIOResult<()> {
    let ch_slices: Vec<&[i32]> = accum.iter().map(|ch| &ch[start..start + len]).collect();
    let frame_bytes = encode_frame_from_channels(
        &ch_slices,
        bits_per_sample,
        sample_rate,
        frame_number,
        params.max_lpc_order,
        params.qlp_precision,
        params.min_partition_order,
        params.max_partition_order,
        params.try_mid_side,
        params.exhaustive_rice,
    )
    .map_err(AudioIOError::FlacError)?;
    writer.write_all(&frame_bytes)?;
    Ok(())
}

impl<W> AudioStreamWriter for StreamedFlacWriter<W>
where
    W: WriteSeek,
{
    fn flush(&mut self) -> AudioIOResult<()> {
        // Flush already-encoded frame bytes; the partial block stays buffered until
        // finalize() so the stream remains a valid fixed-blocksize FLAC stream.
        self.writer.flush()?;
        Ok(())
    }

    fn finalize(&mut self) -> AudioIOResult<()> {
        if self.finalized {
            return Ok(()); // Idempotent
        }

        // Encode any buffered trailing (partial) block as the final frame.
        let remaining = self.accum.first().map(|c| c.len()).unwrap_or(0);
        if remaining > 0 {
            encode_and_write_block(
                &mut self.writer,
                &self.accum,
                0,
                remaining,
                self.bits_per_sample,
                self.sample_rate,
                self.frame_number,
                &self.params,
            )?;
            self.frame_number += 1;
            for ch in self.accum.iter_mut() {
                ch.clear();
            }
        }

        // Back-patch STREAMINFO with the final sample count and block sizes. For a stream
        // shorter than one block, clamp the reported block size to the sample count, just
        // as the bulk encoder does.
        let total = self.frames_written as u64;
        let final_block = if self.frames_written == 0 {
            self.block_size
        } else {
            self.block_size.min(self.frames_written as u32)
        };
        let stream_info = StreamInfo {
            min_block_size: final_block as u16,
            max_block_size: final_block as u16,
            min_frame_size: 0,
            max_frame_size: 0,
            sample_rate: self.sample_rate,
            channels: self.channels as u8,
            bits_per_sample: self.bits_per_sample,
            total_samples: total,
            md5_signature: [0; 16],
        };
        let streaminfo_bytes = stream_info.to_bytes();

        let end = self.writer.stream_position()?;
        self.writer.seek(SeekFrom::Start(self.streaminfo_offset))?;
        self.writer.write_all(&streaminfo_bytes)?;
        self.writer.seek(SeekFrom::Start(end))?;
        self.writer.flush()?;

        self.finalized = true;
        Ok(())
    }

    fn is_finalized(&self) -> bool {
        self.finalized
    }

    fn frames_written(&self) -> usize {
        self.frames_written
    }

    fn sample_rate(&self) -> u32 {
        self.sample_rate
    }

    fn num_channels(&self) -> u16 {
        self.channels
    }
}

impl<W> AudioStreamWrite for StreamedFlacWriter<W>
where
    W: WriteSeek,
{
    fn write_frames<T>(&mut self, samples: &AudioSamples<'_, T>) -> AudioIOResult<usize>
    where
        T: StandardSample + 'static,
    {
        if self.finalized {
            return Err(AudioIOError::corrupted_data_simple(
                "Cannot write to finalized stream",
                "Call write_frames before finalize()",
            ));
        }

        // Validate channel count.
        let input_channels = samples.num_channels();
        if input_channels.get() != self.channels as u32 {
            return Err(AudioIOError::corrupted_data_simple(
                "Channel count mismatch",
                format!(
                    "Writer configured for {} channels, got {} channels",
                    self.channels, input_channels
                ),
            ));
        }

        // Validate bit depth: every chunk must map to the writer's configured depth.
        let chunk_bits: u8 = match T::SAMPLE_TYPE {
            SampleType::I16 => 16,
            _ => 24,
        };
        if chunk_bits != self.bits_per_sample {
            return Err(AudioIOError::corrupted_data_simple(
                "Sample bit-depth mismatch",
                format!(
                    "Writer configured for {}-bit FLAC, but input maps to {}-bit",
                    self.bits_per_sample, chunk_bits
                ),
            ));
        }

        let frames_per_channel = samples.samples_per_channel().get();
        let planar = audio_to_planar_i32(samples)?;
        for (ch, data) in planar.iter().enumerate() {
            self.accum[ch].extend_from_slice(data);
        }

        // Emit every complete block now; keep the remainder buffered.
        let bs = self.block_size as usize;
        let available = self.accum.first().map(|c| c.len()).unwrap_or(0);
        if bs > 0 && available >= bs {
            let full_blocks = available / bs;
            for b in 0..full_blocks {
                encode_and_write_block(
                    &mut self.writer,
                    &self.accum,
                    b * bs,
                    bs,
                    self.bits_per_sample,
                    self.sample_rate,
                    self.frame_number,
                    &self.params,
                )?;
                self.frame_number += 1;
            }
            let consumed = full_blocks * bs;
            for ch in self.accum.iter_mut() {
                ch.drain(0..consumed);
            }
        }

        self.frames_written += frames_per_channel;
        Ok(frames_per_channel)
    }
}

/// Best-effort finalization on drop.
///
/// Unlike the WAV writer, FLAC buffers a partial block in memory, so dropping without
/// finalizing would lose those samples *and* leave STREAMINFO's sample count at zero. We
/// therefore finalize on drop, swallowing errors (a destructor cannot report them) and
/// warning in debug builds. Prefer calling `finalize()` explicitly so I/O errors surface.
impl<W> Drop for StreamedFlacWriter<W>
where
    W: WriteSeek,
{
    fn drop(&mut self) {
        if !self.finalized {
            #[cfg(debug_assertions)]
            if self.frames_written > 0 {
                eprintln!(
                    "Warning: StreamedFlacWriter dropped without calling finalize(); \
                     finalizing now. Call finalize() explicitly to surface I/O errors."
                );
            }
            let _ = self.finalize();
        }
    }
}