Added flac and mp3 output format.
This commit is contained in:
parent
d0a20cd562
commit
d1dca0f477
10 changed files with 745 additions and 128 deletions
168
src/flac.rs
Normal file
168
src/flac.rs
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
/*
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue