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::{BufWriter, 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
58/// Writer that streams recording chunks to a FLAC file.
59///
60/// Pass interleaved `f32` samples (the same format as flexaudio's `AudioChunk.data`) to
61/// [`write_chunk`](FlacWriter::write_chunk), then finalize the header with
62/// [`finalize`](FlacWriter::finalize). Encoding runs synchronously on the caller thread.
63///
64/// Dropping without calling finalize still attempts to close the file (writing any remaining
65/// samples and finalizing the header); errors are swallowed. Call finalize to ensure it is
66/// complete.
67pub struct FlacWriter {
68    file: BufWriter<File>,
69    config: Verified<EncoderConfig>,
70    /// Stream information accumulated for the final header values.
71    stream_info: StreamInfo,
72    /// Input block buffer; flacenc stores channels separately.
73    frame_buf: FrameBuf,
74    /// Accumulates the MD5 and total sample count using flacenc's encoding-independent mechanism.
75    context: Context,
76    /// Reusable sink for serializing frame bits.
77    sink: ByteSink,
78    /// Carries quantized samples that do not yet fill a block.
79    pending: Vec<i32>,
80    channels: usize,
81    /// Number of the next frame to write; frames are sequential because block size is fixed.
82    frame_number: usize,
83    /// If finalized, Drop does nothing.
84    finalized: bool,
85}
86
87impl FlacWriter {
88    /// Create a new 16-bit FLAC file at `path`, overwriting any existing file.
89    ///
90    /// Supported values are `channels` 1..=2 and `sample_rate` 1..=96,000 Hz. Out-of-range
91    /// values return [`EncodeError::Unsupported`] without creating a file.
92    pub fn create<P: AsRef<Path>>(path: P, sample_rate: u32, channels: u16) -> Result<FlacWriter> {
93        if !(1..=2).contains(&channels) {
94            return Err(EncodeError::Unsupported(format!(
95                "channels must be 1 or 2, got {channels}"
96            )));
97        }
98        if !(1..=MAX_SAMPLE_RATE).contains(&sample_rate) {
99            return Err(EncodeError::Unsupported(format!(
100                "sample rate must be 1..={MAX_SAMPLE_RATE} Hz, got {sample_rate}"
101            )));
102        }
103
104        let config = EncoderConfig::default()
105            .into_verified()
106            .map_err(|(_, e)| enc_err(e))?;
107        let mut stream_info =
108            StreamInfo::new(sample_rate as usize, channels as usize, BITS_PER_SAMPLE)
109                .map_err(enc_err)?;
110        // Declare a fixed-block-size stream, matching flacenc's batch entry point.
111        stream_info
112            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
113            .map_err(enc_err)?;
114        let frame_buf = FrameBuf::with_size(channels as usize, BLOCK_SIZE).map_err(enc_err)?;
115        let context = Context::new(BITS_PER_SAMPLE, channels as usize);
116
117        let mut writer = FlacWriter {
118            file: BufWriter::new(File::create(path)?),
119            config,
120            stream_info,
121            frame_buf,
122            context,
123            sink: ByteSink::new(),
124            pending: Vec::new(),
125            channels: channels as usize,
126            frame_number: 0,
127            finalized: false,
128        };
129        // Write a placeholder header; finalize overwrites it with final values of the same length.
130        let header = writer.header_bytes()?;
131        writer.file.write_all(&header)?;
132        Ok(writer)
133    }
134
135    /// Append interleaved `f32` samples.
136    ///
137    /// The length must be a multiple of `channels` (`AudioChunk.data` from flexaudio can be
138    /// passed directly). Otherwise, this returns [`EncodeError::Unsupported`] and writes
139    /// nothing. A partial block is buffered and written by the next call or during finalize.
140    pub fn write_chunk(&mut self, interleaved: &[f32]) -> Result<()> {
141        if interleaved.len() % self.channels != 0 {
142            return Err(EncodeError::Unsupported(format!(
143                "chunk length {} is not a multiple of channels {}",
144                interleaved.len(),
145                self.channels
146            )));
147        }
148        self.pending.reserve(interleaved.len());
149        self.pending
150            .extend(interleaved.iter().map(|&x| quantize_i16(x)));
151        self.drain_full_blocks()
152    }
153
154    /// Write any remaining samples, finalize the header, and close the file.
155    ///
156    /// Consumes `self`, so the type prevents further calls to
157    /// [`write_chunk`](FlacWriter::write_chunk). Drop makes a best-effort attempt at the same
158    /// operation, but only finalize reports write errors, so finalize is recommended.
159    pub fn finalize(mut self) -> Result<()> {
160        let result = self.finish_inner();
161        // Prevent Drop from running this twice; do not retry even if it failed.
162        self.finalized = true;
163        result
164    }
165
166    /// Encode and write all full blocks from pending samples.
167    fn drain_full_blocks(&mut self) -> Result<()> {
168        let block_len = BLOCK_SIZE * self.channels;
169        // encode_block needs &mut self, so pending cannot stay borrowed. Take it out, process
170        // it, then remove the consumed samples when restoring it.
171        let pending = std::mem::take(&mut self.pending);
172        let mut consumed = 0;
173        let mut result = Ok(());
174        while consumed + block_len <= pending.len() {
175            if let Err(e) = self.encode_block(&pending[consumed..consumed + block_len]) {
176                result = Err(e);
177                break;
178            }
179            consumed += block_len;
180        }
181        self.pending = pending;
182        self.pending.drain(..consumed);
183        result
184    }
185
186    /// Encode one block of quantized samples (the final block may be partial) into one frame
187    /// and write it to the file.
188    fn encode_block(&mut self, block: &[i32]) -> Result<()> {
189        self.frame_buf.fill_interleaved(block).map_err(enc_err)?;
190        // Accumulate the MD5 and total sample count here.
191        self.context.fill_interleaved(block).map_err(enc_err)?;
192
193        let frame = flacenc::encode_fixed_size_frame(
194            &self.config,
195            &self.frame_buf,
196            self.frame_number,
197            &self.stream_info,
198        )
199        .map_err(enc_err)?;
200        // Accumulate min/max frame size statistics for the header written during finalize.
201        self.stream_info.update_frame_info(&frame);
202
203        self.sink.clear();
204        frame.write(&mut self.sink).map_err(enc_err)?;
205        // FLAC frames end on byte boundaries, so as_slice returns all of the data.
206        self.file.write_all(self.sink.as_slice())?;
207        self.frame_number += 1;
208        Ok(())
209    }
210
211    /// Implementation of finalize, also called by Drop.
212    fn finish_inner(&mut self) -> Result<()> {
213        // Write remaining samples as the final frame; only the last FLAC frame may be short.
214        if !self.pending.is_empty() {
215            let tail = std::mem::take(&mut self.pending);
216            self.encode_block(&tail)?;
217        }
218
219        // Finalize STREAMINFO.
220        // - Min/max block size: RFC 9639 excludes the final partial block, but
221        //   update_frame_info includes it. If the tail has fewer than 16 samples, this creates
222        //   a non-compliant header that claxon and others reject. Restore the declared value
223        //   because this is a fixed-block-size stream.
224        // - Total samples: update_frame_info also accumulates this, but Context is authoritative.
225        self.stream_info
226            .set_block_sizes(BLOCK_SIZE, BLOCK_SIZE)
227            .map_err(enc_err)?;
228        self.stream_info.set_md5_digest(&self.context.md5_digest());
229        self.stream_info
230            .set_total_samples(self.context.total_samples());
231
232        // Overwrite the placeholder header at the start with the final header of the same length.
233        let header = self.header_bytes()?;
234        self.file.rewind()?;
235        self.file.write_all(&header)?;
236        self.file.flush()?;
237        Ok(())
238    }
239
240    /// Build a FLAC header ("fLaC" + STREAMINFO block) from the current StreamInfo.
241    ///
242    /// Always [`HEADER_LEN`] bytes. finalize overwrites the start of the file, relying on the
243    /// header length staying constant between creation and finalize.
244    fn header_bytes(&self) -> Result<Vec<u8>> {
245        let mut info = self.stream_info.clone();
246        if self.frame_number == 0 {
247            // With no frames, min/max frame sizes remain at flacenc's sentinel values
248            // (u32::MAX / 0). Write them as FLAC's "0 = unknown" instead.
249            info.set_frame_sizes(0, 0).map_err(enc_err)?;
250        }
251        // Serializing a Stream with no frames writes only the magic and STREAMINFO block.
252        let stream = Stream::with_stream_info(info);
253        let mut sink = ByteSink::new();
254        stream.write(&mut sink).map_err(enc_err)?;
255        let bytes = sink.into_inner();
256        debug_assert_eq!(bytes.len(), HEADER_LEN);
257        Ok(bytes)
258    }
259}
260
261impl Drop for FlacWriter {
262    fn drop(&mut self) {
263        if !self.finalized {
264            // Best effort; swallow errors. Call finalize to detect them.
265            let _ = self.finish_inner();
266        }
267    }
268}
269
270// flacenc types such as Verified do not implement Debug, so implement it manually.
271impl std::fmt::Debug for FlacWriter {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        f.debug_struct("FlacWriter")
274            .field("sample_rate", &self.stream_info.sample_rate())
275            .field("channels", &self.channels)
276            .field("frame_number", &self.frame_number)
277            .field("pending_samples", &self.pending.len())
278            .field("finalized", &self.finalized)
279            .finish_non_exhaustive()
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use super::quantize_i16;
286
287    #[test]
288    fn quantize_reference_points() {
289        assert_eq!(quantize_i16(0.0), 0);
290        assert_eq!(quantize_i16(0.25), 8192);
291        assert_eq!(quantize_i16(-1.0), -32768);
292        // Positive full scale does not fit in 16 bits, so clamp it.
293        assert_eq!(quantize_i16(1.0), 32767);
294        // Out-of-range values saturate.
295        assert_eq!(quantize_i16(2.0), 32767);
296        assert_eq!(quantize_i16(-2.0), -32768);
297        // Non-finite values.
298        assert_eq!(quantize_i16(f32::NAN), 0);
299        assert_eq!(quantize_i16(f32::INFINITY), 32767);
300        assert_eq!(quantize_i16(f32::NEG_INFINITY), -32768);
301        // Round to nearest (round half away from zero). 1.5/32768 is exact in binary.
302        assert_eq!(quantize_i16(1.5 / 32768.0), 2);
303        assert_eq!(quantize_i16(-1.5 / 32768.0), -2);
304    }
305}