168 lines
5.7 KiB
Rust
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);
|
|
}
|
|
}
|
|
}
|