/* * This file is part of tuxrecord2. * https://xw3.org/hanez/tuxrecord2 * * Real-time FLAC encoding through a minimal FFI shim over the system * libFLAC stream encoder. Captured PCM buffers are fed directly to * libFLAC as they arrive, so no intermediate WAV file is needed. * * Copyright 2026 Johannes Findeisen * Licensed under the terms of the Apache-2.0 license. */ use std::ffi::CString; use std::os::raw::{c_char, c_int, c_uint, c_void}; use std::os::unix::ffi::OsStrExt; use std::path::Path; /// Compression level passed to libFLAC (0 = fastest, 8 = best). const COMPRESSION_LEVEL: u32 = 5; /// Opaque `FLAC__StreamEncoder`. #[repr(C)] struct StreamEncoder { _private: [u8; 0], } // Safety: the encoder handle is only ever used from the thread that owns // it; tuxrecord2 is single-threaded. unsafe impl Send for StreamEncoder {} extern "C" { fn FLAC__stream_encoder_new() -> *mut StreamEncoder; fn FLAC__stream_encoder_delete(encoder: *mut StreamEncoder); fn FLAC__stream_encoder_set_compression_level( encoder: *mut StreamEncoder, value: c_uint, ) -> c_int; fn FLAC__stream_encoder_set_channels(encoder: *mut StreamEncoder, value: c_uint) -> c_int; fn FLAC__stream_encoder_set_bits_per_sample( encoder: *mut StreamEncoder, value: c_uint, ) -> c_int; fn FLAC__stream_encoder_set_sample_rate(encoder: *mut StreamEncoder, value: c_uint) -> c_int; fn FLAC__stream_encoder_init_file( encoder: *mut StreamEncoder, filename: *const c_char, progress_callback: *const c_void, client_data: *mut c_void, ) -> c_int; fn FLAC__stream_encoder_process_interleaved( encoder: *mut StreamEncoder, buffer: *const i32, samples: c_uint, ) -> c_int; fn FLAC__stream_encoder_finish(encoder: *mut StreamEncoder) -> c_int; } /* * Real-time FLAC encoder. * * Owns a libFLAC stream encoder writing to the given path. Interleaved * S16_LE buffers are expanded into the i32 layout libFLAC expects using a * reusable conversion buffer, then handed to the encoder; the final * STREAMINFO block is written by finish(). */ pub struct FlacEncoder { encoder: *mut StreamEncoder, channels: usize, /// Reusable i32 conversion buffer (the processing buffer). buffer: Vec, finished: bool, } impl FlacEncoder { pub fn create(path: &Path, rate: u32, channels: u32, bits: u32) -> Result { let filename = CString::new(path.as_os_str().as_bytes()) .map_err(|_| "invalid output path".to_string())?; // SAFETY: all libFLAC calls are made on a freshly allocated encoder // and the return values checked before use. unsafe { let encoder = FLAC__stream_encoder_new(); if encoder.is_null() { return Err("could not allocate the FLAC encoder".to_string()); } let ok = FLAC__stream_encoder_set_compression_level(encoder, COMPRESSION_LEVEL) != 0 && FLAC__stream_encoder_set_channels(encoder, channels) != 0 && FLAC__stream_encoder_set_bits_per_sample(encoder, bits) != 0 && FLAC__stream_encoder_set_sample_rate(encoder, rate) != 0; if !ok { FLAC__stream_encoder_delete(encoder); return Err("could not configure the FLAC encoder".to_string()); } let status = FLAC__stream_encoder_init_file( encoder, filename.as_ptr(), std::ptr::null(), std::ptr::null_mut(), ); if status != 0 { FLAC__stream_encoder_delete(encoder); return Err(format!( "could not open the FLAC output file (status {status})" )); } Ok(FlacEncoder { encoder, channels: channels as usize, buffer: Vec::new(), finished: false, }) } } /// Encode one buffer of interleaved S16_LE samples. pub fn write_samples(&mut self, samples: &[i16]) -> Result<(), String> { if self.finished { return Err("FLAC encoder already finished".to_string()); } if self.channels == 0 || samples.is_empty() { return Ok(()); } self.buffer.clear(); self.buffer.extend(samples.iter().map(|&s| i32::from(s))); // libFLAC counts frames (samples per channel), not interleaved items. let frames = (self.buffer.len() / self.channels) as c_uint; // SAFETY: buffer holds exactly frames * channels valid i32 samples. let ok = unsafe { FLAC__stream_encoder_process_interleaved(self.encoder, self.buffer.as_ptr(), frames) }; if ok == 0 { return Err("FLAC encoding failed".to_string()); } Ok(()) } /// Flush the remaining frames and write the final STREAMINFO block. pub fn finish(&mut self) -> Result<(), String> { if self.finished { return Ok(()); } self.finished = true; // SAFETY: self.encoder is a live encoder initialised in create(). let ok = unsafe { FLAC__stream_encoder_finish(self.encoder) }; if ok == 0 { return Err("could not finalize the FLAC stream".to_string()); } Ok(()) } } impl Drop for FlacEncoder { fn drop(&mut self) { if !self.finished { let _ = self.finish(); } // SAFETY: the encoder is deleted exactly once; its pointer is never // used again afterwards. unsafe { FLAC__stream_encoder_delete(self.encoder); } } }