Skip to main content

flexaudio_encode/
writer.rs

1//! FLAC streaming writer ([`FlacWriter`]).
2//!
3//! flacenc's main entry point, `encode_with_fixed_block_size`, reads all samples from
4//! [`Source`] at once, which would keep an entire long recording in memory. This uses the
5//! frame-level entry point [`flacenc::encode_fixed_size_frame`] and encodes each block
6//! (4096 samples/channel) as soon as it is full, writing it to the file.
7//!
8//! The STREAMINFO header is initially written with placeholder values (zero total samples
9//! and a zero MD5), then [`FlacWriter::finalize`] seeks to the start and replaces them with
10//! the final values. STREAMINFO has a fixed length of 34 bytes, so the full header is always
11//! 42 bytes and can be safely overwritten.
12//!
13//! [`Source`]: flacenc::source::Source
14
15use std::fs::File;
16use std::io::{Seek, Write};
17use std::path::Path;
18
19use flacenc::bitsink::ByteSink;
20use flacenc::component::{BitRepr, Stream, StreamInfo};
21use flacenc::config::Encoder as EncoderConfig;
22use flacenc::error::{Verified, Verify};
23use flacenc::source::{Context, Fill, FrameBuf};
24
25use crate::error::{EncodeError, Result};
26
27/// Block size per frame (samples per channel), matching flacenc's default.
28const BLOCK_SIZE: usize = 4096;
29
30/// Quantization bit depth. Currently fixed at 16 bits.
31/// flacenc supports up to 24 bits, so 24-bit FLAC could be written by expanding the scale
32/// in [`quantize_i16`] and this value if needed (not currently implemented).
33const BITS_PER_SAMPLE: usize = 16;
34
35/// Maximum supported sample rate (Hz). FLAC itself can represent up to 655,350 Hz, but
36/// flacenc's validation is limited to 96 kHz, so this matches that limit.
37const MAX_SAMPLE_RATE: u32 = 96_000;
38
39/// Total FLAC header length: "fLaC" magic (4 bytes) + metadata block header (4 bytes) + STREAMINFO (34 bytes).
40const HEADER_LEN: usize = 42;
41
42/// Quantize one f32 sample to a 16-bit integer (simple quantization without dithering).
43///
44/// The canonical quantizer is [`flexaudio_core::quantize_i16`], shared across all layers.
45/// flacenc's API requires `i32`, so this delegates to the core `i16` version and widens the
46/// result. The scale is 32768 (negative full scale is based on -1.0); `+1.0` is clamped to
47/// 32767, out-of-range values saturate, and NaN becomes 0.
48#[inline]
49fn quantize_i16(x: f32) -> i32 {
50    flexaudio_core::quantize_i16(x) as i32
51}
52
53/// Helper to map flacenc errors to [`EncodeError::Encoder`].
54fn enc_err(e: impl std::fmt::Display) -> EncodeError {
55    EncodeError::Encoder(e.to_string())
56}
57
58trait Output: Write + Seek + Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe {}
59impl<T: Write + Seek + Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe> Output
60    for T
61{
62}
63
64#[derive(Debug)]
65enum WriterState {
66    Open,
67    Finalized,
68    Failed(FailureCause),
69}
70
71// File errors contain an OS code or a kind/message. Snapshot these diagnostics because
72// io::Error cannot be cloned, preserving both the first cause and the writer's auto traits.
73#[derive(Debug)]
74enum FailureCause {
75    Io {
76        code: Option<i32>,
77        kind: std::io::ErrorKind,
78        message: String,
79    },
80    Unsupported(String),
81    Encoder(String),
82}
83
84impl FailureCause {
85    fn capture(error: EncodeError) -> Self {
86        match error {
87            EncodeError::Io(error) => Self::Io {
88                code: error.raw_os_error(),
89                kind: error.kind(),
90                message: error.to_string(),
91            },
92            EncodeError::Unsupported(message) => Self::Unsupported(message),
93            EncodeError::Encoder(message) => Self::Encoder(message),
94        }
95    }
96
97    fn error(&self) -> EncodeError {
98        match self {
99            Self::Io {
100                code,
101                kind,
102                message,
103            } => EncodeError::Io(match code {
104                Some(code) => std::io::Error::from_raw_os_error(*code),
105                None => std::io::Error::new(*kind, message.clone()),
106            }),
107            Self::Unsupported(message) => EncodeError::Unsupported(message.clone()),
108            Self::Encoder(message) => EncodeError::Encoder(message.clone()),
109        }
110    }
111}
112
113/// Writer that streams recording chunks to a FLAC file.
114///
115/// Pass interleaved `f32` samples (the same format as flexaudio's `AudioChunk.data`) to
116/// [`write_chunk`](FlacWriter::write_chunk), then finalize the header with
117/// [`finalize`](FlacWriter::finalize). Encoding runs synchronously on the caller thread.
118///
119/// Dropping an open writer without calling finalize still attempts to close the file
120/// (writing remaining samples and finalizing the header). Drop never retries a failed
121/// writer and never panics; call finalize to detect errors.
122pub struct FlacWriter {
123    // Unbuffered output avoids BufWriter retrying buffered bytes when a failed writer drops.
124    file: Box<dyn Output>,
125    config: Verified<EncoderConfig>,
126    /// Stream information accumulated for the final header values.
127    stream_info: StreamInfo,
128    /// Input block buffer; flacenc stores channels separately.
129    frame_buf: FrameBuf,
130    /// Accumulates the MD5 and total sample count using flacenc's encoding-independent mechanism.
131    context: Context,
132    /// Reusable sink for serializing frame bits.
133    sink: ByteSink,
134    /// Carries quantized samples that do not yet fill a block.
135    pending: Vec<i32>,
136    channels: usize,
137    /// Number of the next frame to write; frames are sequential because block size is fixed.
138    frame_number: usize,
139    state: WriterState,
140}
141
142impl FlacWriter {
143    /// Create a new 16-bit FLAC file at `path`, overwriting any existing file.
144    ///
145    /// Supported values are `channels` 1..=2 and `sample_rate` 1..=96,000 Hz. Out-of-range
146    /// values return [`EncodeError::Unsupported`] without creating a file.
147    pub fn create<P: AsRef<Path>>(path: P, sample_rate: u32, channels: u16) -> Result<FlacWriter> {
148        if !(1..=2).contains(&channels) {
149            return Err(EncodeError::Unsupported(format!(
150                "channels must be 1 or 2, got {channels}"
151            )));
152        }
153        if !(1..=MAX_SAMPLE_RATE).contains(&sample_rate) {
154            return Err(EncodeError::Unsupported(format!(
155                "sample rate must be 1..={MAX_SAMPLE_RATE} Hz, got {sample_rate}"
156            )));
157        }
158
159        Self::with_output(sample_rate, channels, Box::new(File::create(path)?))
160    }
161
162    fn with_output(sample_rate: u32, channels: u16, file: Box<dyn Output>) -> Result<Self> {
163        let config = EncoderConfig::default()
164            .into_verified()
165            .map_err(|(_, e)| enc_err(e))?;
166        let mut stream_info =
167            StreamInfo::new(sample_rate as usize, channels as usize, BITS_PER_SAMPLE)
168                .map_err(enc_err)?;
169        // Declare a fixed-block-size stream, matching flacenc's batch entry point.
170        stream_info
171            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
172            .map_err(enc_err)?;
173        let frame_buf = FrameBuf::with_size(channels as usize, BLOCK_SIZE).map_err(enc_err)?;
174        let context = Context::new(BITS_PER_SAMPLE, channels as usize);
175
176        let mut writer = FlacWriter {
177            file,
178            config,
179            stream_info,
180            frame_buf,
181            context,
182            sink: ByteSink::new(),
183            pending: Vec::new(),
184            channels: channels as usize,
185            frame_number: 0,
186            state: WriterState::Open,
187        };
188        // Write a placeholder header; finalize overwrites it with final values of the same length.
189        let header = writer.header_bytes()?;
190        let result = writer.file.write_all(&header).map_err(EncodeError::Io);
191        writer.latch(result)?;
192        Ok(writer)
193    }
194
195    /// Append interleaved `f32` samples.
196    ///
197    /// The length must be a multiple of `channels` (`AudioChunk.data` from flexaudio can be
198    /// passed directly). Otherwise, this returns [`EncodeError::Unsupported`] and writes
199    /// nothing. Encoding or I/O failure permanently fails the writer; later writes and
200    /// finalize return the first cause. A partial block is buffered and written by the next call or during finalize.
201    pub fn write_chunk(&mut self, interleaved: &[f32]) -> Result<()> {
202        self.check_open()?;
203        if interleaved.len() % self.channels != 0 {
204            return Err(EncodeError::Unsupported(format!(
205                "chunk length {} is not a multiple of channels {}",
206                interleaved.len(),
207                self.channels
208            )));
209        }
210        self.pending.reserve(interleaved.len());
211        self.pending
212            .extend(interleaved.iter().map(|&x| quantize_i16(x)));
213        let result = self.drain_full_blocks();
214        self.latch(result)
215    }
216
217    /// Write any remaining samples, finalize the header, and close the file.
218    ///
219    /// Consumes `self`, so the type prevents further calls to
220    /// [`write_chunk`](FlacWriter::write_chunk). Drop makes a best-effort attempt at the same
221    /// operation, but only finalize reports write errors, so finalize is recommended.
222    pub fn finalize(mut self) -> Result<()> {
223        self.finish()
224    }
225
226    fn check_open(&self) -> Result<()> {
227        match &self.state {
228            WriterState::Open => Ok(()),
229            WriterState::Finalized => Err(EncodeError::Unsupported("writer is finalized".into())),
230            WriterState::Failed(error) => Err(error.error()),
231        }
232    }
233
234    fn latch(&mut self, result: Result<()>) -> Result<()> {
235        if let Err(error) = result {
236            self.state = WriterState::Failed(FailureCause::capture(error));
237            return self.check_open();
238        }
239        Ok(())
240    }
241
242    fn finish(&mut self) -> Result<()> {
243        self.check_open()?;
244        let result = self.finish_inner();
245        self.latch(result)?;
246        self.state = WriterState::Finalized;
247        Ok(())
248    }
249
250    /// Encode and write all full blocks from pending samples.
251    fn drain_full_blocks(&mut self) -> Result<()> {
252        let block_len = BLOCK_SIZE * self.channels;
253        // encode_block needs &mut self, so pending cannot stay borrowed. Take it out, process
254        // it, then remove the consumed samples when restoring it.
255        let pending = std::mem::take(&mut self.pending);
256        let mut consumed = 0;
257        let mut result = Ok(());
258        while consumed + block_len <= pending.len() {
259            if let Err(e) = self.encode_block(&pending[consumed..consumed + block_len]) {
260                result = Err(e);
261                break;
262            }
263            consumed += block_len;
264        }
265        self.pending = pending;
266        self.pending.drain(..consumed);
267        result
268    }
269
270    /// Encode one block of quantized samples (the final block may be partial) into one frame
271    /// and write it to the file.
272    fn encode_block(&mut self, block: &[i32]) -> Result<()> {
273        self.frame_buf.fill_interleaved(block).map_err(enc_err)?;
274        // Accumulate the MD5 and total sample count here.
275        self.context.fill_interleaved(block).map_err(enc_err)?;
276
277        let frame = flacenc::encode_fixed_size_frame(
278            &self.config,
279            &self.frame_buf,
280            self.frame_number,
281            &self.stream_info,
282        )
283        .map_err(enc_err)?;
284        // Accumulate min/max frame size statistics for the header written during finalize.
285        self.stream_info.update_frame_info(&frame);
286
287        self.sink.clear();
288        frame.write(&mut self.sink).map_err(enc_err)?;
289        // FLAC frames end on byte boundaries, so as_slice returns all of the data.
290        self.file.write_all(self.sink.as_slice())?;
291        self.frame_number += 1;
292        Ok(())
293    }
294
295    /// Implementation of finalize, also called by Drop.
296    fn finish_inner(&mut self) -> Result<()> {
297        // Write remaining samples as the final frame; only the last FLAC frame may be short.
298        if !self.pending.is_empty() {
299            let tail = std::mem::take(&mut self.pending);
300            self.encode_block(&tail)?;
301        }
302
303        // Finalize STREAMINFO.
304        // - Min/max block size: RFC 9639 excludes the final partial block, but
305        //   update_frame_info includes it. If the tail has fewer than 16 samples, this creates
306        //   a non-compliant header that claxon and others reject. Restore the declared value
307        //   because this is a fixed-block-size stream.
308        // - Total samples: update_frame_info also accumulates this, but Context is authoritative.
309        self.stream_info
310            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
311            .map_err(enc_err)?;
312        self.stream_info.set_md5_digest(&self.context.md5_digest());
313        self.stream_info
314            .set_total_samples(self.context.total_samples());
315
316        // Overwrite the placeholder header at the start with the final header of the same length.
317        let header = self.header_bytes()?;
318        // Publish the FLAC magic last. Until that commit succeeds, even permanently failed
319        // output cannot look finalized. File is unbuffered; no fallible work follows commit.
320        self.file.flush()?;
321        self.file.rewind()?;
322        self.file.write_all(b"FAIL")?;
323        self.file.write_all(&header[4..])?;
324        self.file.flush()?;
325        self.file.rewind()?;
326        self.file.write_all(&header[..4])?;
327        Ok(())
328    }
329
330    /// Build a FLAC header ("fLaC" + STREAMINFO block) from the current StreamInfo.
331    ///
332    /// Always [`HEADER_LEN`] bytes. finalize overwrites the start of the file, relying on the
333    /// header length staying constant between creation and finalize.
334    fn header_bytes(&self) -> Result<Vec<u8>> {
335        let mut info = self.stream_info.clone();
336        if self.frame_number == 0 {
337            // With no frames, min/max frame sizes remain at flacenc's sentinel values
338            // (u32::MAX / 0). Write them as FLAC's "0 = unknown" instead.
339            info.set_frame_sizes(0, 0).map_err(enc_err)?;
340        }
341        // Serializing a Stream with no frames writes only the magic and STREAMINFO block.
342        let stream = Stream::with_stream_info(info);
343        let mut sink = ByteSink::new();
344        stream.write(&mut sink).map_err(enc_err)?;
345        let bytes = sink.into_inner();
346        debug_assert_eq!(bytes.len(), HEADER_LEN);
347        Ok(bytes)
348    }
349}
350
351impl Drop for FlacWriter {
352    fn drop(&mut self) {
353        if matches!(self.state, WriterState::Open) {
354            // Best effort; swallow errors. Call finalize to detect them.
355            let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| self.finish()));
356        }
357    }
358}
359
360// flacenc types such as Verified do not implement Debug, so implement it manually.
361impl std::fmt::Debug for FlacWriter {
362    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
363        f.debug_struct("FlacWriter")
364            .field("sample_rate", &self.stream_info.sample_rate())
365            .field("channels", &self.channels)
366            .field("frame_number", &self.frame_number)
367            .field("pending_samples", &self.pending.len())
368            .field("state", &self.state)
369            .finish_non_exhaustive()
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use std::io::{Cursor, SeekFrom};
377    use std::sync::{Arc, Mutex};
378
379    struct FaultOutput {
380        data: Arc<Mutex<Cursor<Vec<u8>>>>,
381        calls: Arc<Mutex<usize>>,
382        fail_at: usize,
383        partial: bool,
384        fail_flush_at: usize,
385        flushes: usize,
386    }
387
388    impl Write for FaultOutput {
389        fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
390            let mut calls = self.calls.lock().unwrap();
391            *calls += 1;
392            if *calls == self.fail_at {
393                if self.partial {
394                    return self.data.lock().unwrap().write(&bytes[..bytes.len() / 2]);
395                }
396                return Err(std::io::Error::other("injected first failure"));
397            }
398            if self.partial && *calls == self.fail_at + 1 {
399                return Err(std::io::Error::other("injected first failure"));
400            }
401            self.data.lock().unwrap().write(bytes)
402        }
403        fn flush(&mut self) -> std::io::Result<()> {
404            self.flushes += 1;
405            if self.flushes == self.fail_flush_at {
406                Err(std::io::Error::other("injected flush failure"))
407            } else {
408                Ok(())
409            }
410        }
411    }
412    impl Seek for FaultOutput {
413        fn seek(&mut self, position: SeekFrom) -> std::io::Result<u64> {
414            self.data.lock().unwrap().seek(position)
415        }
416    }
417
418    #[test]
419    fn failed_frame_is_never_retried_or_finalized() {
420        for partial in [false, true] {
421            let data = Arc::new(Mutex::new(Cursor::new(Vec::new())));
422            let calls = Arc::new(Mutex::new(0));
423            let output = FaultOutput {
424                data: data.clone(),
425                calls: calls.clone(),
426                fail_at: 3,
427                partial,
428                fail_flush_at: usize::MAX,
429                flushes: 0,
430            };
431            let mut writer = FlacWriter::with_output(48_000, 1, Box::new(output)).unwrap();
432            writer.write_chunk(&vec![0.25; BLOCK_SIZE]).unwrap();
433            let first = writer
434                .write_chunk(&vec![0.5; BLOCK_SIZE])
435                .unwrap_err()
436                .to_string();
437            let count = writer.context.total_samples();
438            let digest = writer.context.md5_digest();
439            let bytes = data.lock().unwrap().get_ref().clone();
440            let writes = *calls.lock().unwrap();
441            // Even invalid input after failure reports the original cause.
442            assert_eq!(writer.write_chunk(&[]).unwrap_err().to_string(), first);
443            assert_eq!(writer.finish().unwrap_err().to_string(), first);
444            assert_eq!(writer.context.total_samples(), count);
445            assert_eq!(writer.context.md5_digest(), digest);
446            assert_eq!(writer.finalize().unwrap_err().to_string(), first);
447            assert_eq!(*calls.lock().unwrap(), writes);
448            assert_eq!(*data.lock().unwrap().get_ref(), bytes);
449            // Placeholder STREAMINFO still advertises zero total samples and zero digest.
450            assert!(bytes[22..42].iter().all(|&byte| byte == 0));
451        }
452    }
453
454    #[test]
455    fn failed_drop_and_header_flush_do_not_retry() {
456        let data = Arc::new(Mutex::new(Cursor::new(Vec::new())));
457        let calls = Arc::new(Mutex::new(0));
458        let mut writer = FlacWriter::with_output(
459            48_000,
460            1,
461            Box::new(FaultOutput {
462                data: data.clone(),
463                calls: calls.clone(),
464                fail_at: 2,
465                partial: false,
466                fail_flush_at: usize::MAX,
467                flushes: 0,
468            }),
469        )
470        .unwrap();
471        assert!(writer.write_chunk(&vec![0.25; BLOCK_SIZE]).is_err());
472        let before = *calls.lock().unwrap();
473        drop(writer);
474        assert_eq!(*calls.lock().unwrap(), before);
475
476        let mut writer = FlacWriter::with_output(
477            48_000,
478            1,
479            Box::new(FaultOutput {
480                data: data.clone(),
481                calls: calls.clone(),
482                fail_at: usize::MAX,
483                partial: false,
484                fail_flush_at: 1,
485                flushes: 0,
486            }),
487        )
488        .unwrap();
489        writer.write_chunk(&[0.25]).unwrap();
490        let first = writer.finish().unwrap_err().to_string();
491        assert!(first.contains("flush failure"));
492        let before = *calls.lock().unwrap();
493        assert_eq!(writer.finalize().unwrap_err().to_string(), first);
494        assert_eq!(*calls.lock().unwrap(), before);
495    }
496
497    #[test]
498    fn header_publication_failure_invalidates_magic_and_latches() {
499        // Fail metadata publication (including partial write), metadata flush, or magic commit.
500        for (fail_at, partial, fail_flush_at) in [
501            (4, false, usize::MAX),
502            (4, true, usize::MAX),
503            (usize::MAX, false, 2),
504            (5, false, usize::MAX),
505            (5, true, usize::MAX),
506        ] {
507            let data = Arc::new(Mutex::new(Cursor::new(Vec::new())));
508            let calls = Arc::new(Mutex::new(0));
509            let mut writer = FlacWriter::with_output(
510                48_000,
511                1,
512                Box::new(FaultOutput {
513                    data: data.clone(),
514                    calls: calls.clone(),
515                    fail_at,
516                    partial,
517                    fail_flush_at,
518                    flushes: 0,
519                }),
520            )
521            .unwrap();
522            writer.write_chunk(&[0.25]).unwrap();
523            let first = writer.finish().unwrap_err().to_string();
524            assert_ne!(&data.lock().unwrap().get_ref()[..4], b"fLaC");
525            let before = *calls.lock().unwrap();
526            assert_eq!(writer.finalize().unwrap_err().to_string(), first);
527            assert_eq!(*calls.lock().unwrap(), before);
528        }
529    }
530
531    #[test]
532    fn encoder_failure_and_auto_traits() {
533        fn assert_send_sync<T: Send + Sync + std::panic::UnwindSafe + std::panic::RefUnwindSafe>() {
534        }
535        assert_send_sync::<FlacWriter>();
536        let data = Arc::new(Mutex::new(Cursor::new(Vec::new())));
537        let calls = Arc::new(Mutex::new(0));
538        let mut writer = FlacWriter::with_output(
539            48_000,
540            1,
541            Box::new(FaultOutput {
542                data,
543                calls: calls.clone(),
544                fail_at: usize::MAX,
545                partial: false,
546                fail_flush_at: usize::MAX,
547                flushes: 0,
548            }),
549        )
550        .unwrap();
551        writer.frame_number = usize::MAX;
552        let first = writer.write_chunk(&vec![0.25; BLOCK_SIZE]).unwrap_err();
553        assert!(matches!(first, EncodeError::Encoder(_)));
554        let count = writer.context.total_samples();
555        let digest = writer.context.md5_digest();
556        assert_eq!(
557            writer.write_chunk(&[]).unwrap_err().to_string(),
558            first.to_string()
559        );
560        assert_eq!(writer.context.total_samples(), count);
561        assert_eq!(writer.context.md5_digest(), digest);
562        assert_eq!(
563            writer.finalize().unwrap_err().to_string(),
564            first.to_string()
565        );
566        assert_eq!(*calls.lock().unwrap(), 1);
567    }
568
569    #[test]
570    fn invalid_chunk_does_not_fail_an_open_writer() {
571        let data = Arc::new(Mutex::new(Cursor::new(Vec::new())));
572        let output = FaultOutput {
573            data,
574            calls: Arc::new(Mutex::new(0)),
575            fail_at: usize::MAX,
576            partial: false,
577            fail_flush_at: usize::MAX,
578            flushes: 0,
579        };
580        let mut writer = FlacWriter::with_output(48_000, 2, Box::new(output)).unwrap();
581        assert!(writer.write_chunk(&[0.5]).is_err());
582        writer.write_chunk(&[0.5, 0.5]).unwrap();
583        writer.finalize().unwrap();
584    }
585
586    #[test]
587    fn quantize_reference_points() {
588        assert_eq!(quantize_i16(0.0), 0);
589        assert_eq!(quantize_i16(0.25), 8192);
590        assert_eq!(quantize_i16(-1.0), -32768);
591        // Positive full scale does not fit in 16 bits, so clamp it.
592        assert_eq!(quantize_i16(1.0), 32767);
593        // Out-of-range values saturate.
594        assert_eq!(quantize_i16(2.0), 32767);
595        assert_eq!(quantize_i16(-2.0), -32768);
596        // Non-finite values.
597        assert_eq!(quantize_i16(f32::NAN), 0);
598        assert_eq!(quantize_i16(f32::INFINITY), 32767);
599        assert_eq!(quantize_i16(f32::NEG_INFINITY), -32768);
600        // Round to nearest (round half away from zero). 1.5/32768 is exact in binary.
601        assert_eq!(quantize_i16(1.5 / 32768.0), 2);
602        assert_eq!(quantize_i16(-1.5 / 32768.0), -2);
603    }
604}