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}