tuxrecord2/src/flac.rs

168 lines
5.7 KiB
Rust

/*
* 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 <you@hanez.org>
* 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<i32>,
finished: bool,
}
impl FlacEncoder {
pub fn create(path: &Path, rate: u32, channels: u32, bits: u32) -> Result<FlacEncoder, String> {
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);
}
}
}