pmoflac corrections

This commit is contained in:
2025-10-27 17:05:36 +01:00
parent fe984edb7b
commit dd5ed5e890
7 changed files with 698 additions and 3 deletions

View File

@@ -15,23 +15,57 @@ use crate::{
util::interleaved_i32_to_le_bytes,
};
/// Size of chunks when reading FLAC input data (32 KB).
const INGEST_CHUNK_SIZE: usize = 32 * 1024;
/// Channel capacity for async message passing between tasks.
const CHANNEL_CAPACITY: usize = 8;
/// An async stream that decodes FLAC audio into PCM samples.
///
/// This struct implements `AsyncRead`, allowing you to read decoded PCM data
/// as it becomes available. The decoding happens in a background task.
///
/// # Example
///
/// ```no_run
/// use pmoflac::decode_flac_stream;
/// use tokio::fs::File;
/// use tokio::io::AsyncReadExt;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let file = File::open("audio.flac").await?;
/// let mut stream = decode_flac_stream(file).await?;
///
/// println!("Sample rate: {}", stream.info().sample_rate);
///
/// let mut pcm = Vec::new();
/// stream.read_to_end(&mut pcm).await?;
/// stream.wait().await?;
/// # Ok(())
/// # }
/// ```
pub struct FlacDecodedStream {
info: StreamInfo,
reader: ManagedAsyncReader,
}
impl FlacDecodedStream {
/// Returns metadata about the FLAC stream (sample rate, channels, etc.).
pub fn info(&self) -> &StreamInfo {
&self.info
}
/// Consumes the stream and returns its components.
pub fn into_parts(self) -> (StreamInfo, ManagedAsyncReader) {
(self.info, self.reader)
}
/// Waits for the background decoding task to complete.
///
/// This should be called after reading all data to ensure proper cleanup
/// and to catch any errors that occurred during decoding.
pub async fn wait(self) -> Result<(), FlacError> {
self.reader.wait().await
}
@@ -47,6 +81,57 @@ impl AsyncRead for FlacDecodedStream {
}
}
/// Decodes a FLAC stream into PCM audio data.
///
/// This function spawns background tasks to perform the decoding asynchronously.
/// The returned `FlacDecodedStream` implements `AsyncRead` for streaming the PCM output.
///
/// # Threading Model
///
/// - A Tokio task reads chunks from the input and forwards them via a channel
/// - A blocking task (via `spawn_blocking`) runs the FLAC decoder (claxon)
/// - Another Tokio task writes decoded PCM to an internal duplex stream
///
/// This architecture ensures true streaming: output is produced as input is consumed,
/// without buffering entire files.
///
/// # Arguments
///
/// * `reader` - Any async reader containing FLAC-encoded data
///
/// # Returns
///
/// A `FlacDecodedStream` that can be read to obtain PCM samples in little-endian
/// interleaved format. The stream's `info()` method provides metadata.
///
/// # Errors
///
/// Returns an error if:
/// - The input is not valid FLAC data
/// - An I/O error occurs while reading
/// - The decoder encounters corrupted data
///
/// # Example
///
/// ```no_run
/// use pmoflac::decode_flac_stream;
/// use tokio::io::AsyncReadExt;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let flac_data: &[u8] = &[/* ... */];
/// let mut stream = decode_flac_stream(flac_data).await?;
///
/// let info = stream.info().clone();
/// println!("{} Hz, {} channels, {} bits/sample",
/// info.sample_rate, info.channels, info.bits_per_sample);
///
/// let mut pcm = Vec::new();
/// stream.read_to_end(&mut pcm).await?;
/// stream.wait().await?;
/// # Ok(())
/// # }
/// ```
pub async fn decode_flac_stream<R>(reader: R) -> Result<FlacDecodedStream, FlacError>
where
R: AsyncRead + Unpin + Send + 'static,

View File

@@ -17,23 +17,59 @@ use crate::{
util::le_bytes_to_interleaved_i32,
};
/// Channel capacity for async message passing between tasks.
const CHANNEL_CAPACITY: usize = 8;
/// Number of PCM frames to process per chunk (4096 frames = ~93ms at 44.1kHz).
const PCM_FRAMES_PER_CHUNK: usize = 4096;
/// An async stream that encodes PCM audio into FLAC format.
///
/// This struct implements `AsyncRead`, allowing you to read encoded FLAC data
/// as it becomes available. The encoding happens in a background task.
///
/// # Example
///
/// ```no_run
/// use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
/// use tokio::io::AsyncReadExt;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let pcm_data: &[u8] = &[/* 16-bit stereo PCM */];
/// let format = PcmFormat {
/// sample_rate: 44_100,
/// channels: 2,
/// bits_per_sample: 16,
/// };
///
/// let mut stream = encode_flac_stream(pcm_data, format, EncoderOptions::default()).await?;
/// let mut flac_output = Vec::new();
/// stream.read_to_end(&mut flac_output).await?;
/// stream.wait().await?;
/// # Ok(())
/// # }
/// ```
pub struct FlacEncodedStream {
format: PcmFormat,
reader: ManagedAsyncReader,
}
impl FlacEncodedStream {
/// Returns the PCM format used for encoding.
pub fn format(&self) -> PcmFormat {
self.format
}
/// Consumes the stream and returns its components.
pub fn into_parts(self) -> (PcmFormat, ManagedAsyncReader) {
(self.format, self.reader)
}
/// Waits for the background encoding task to complete.
///
/// This should be called after reading all data to ensure proper cleanup
/// and to catch any errors that occurred during encoding.
pub async fn wait(self) -> Result<(), FlacError> {
self.reader.wait().await
}
@@ -49,11 +85,22 @@ impl tokio::io::AsyncRead for FlacEncodedStream {
}
}
/// Options for configuring FLAC encoding.
#[derive(Debug, Clone)]
pub struct EncoderOptions {
/// Compression level (0-12). Higher means better compression but slower.
/// Default: 5 (balanced)
pub compression_level: u32,
/// Whether to verify the encoding by decoding in parallel.
/// Default: false (disabled for performance)
pub verify: bool,
/// Total number of samples (optional). If known, improves seeking in output.
pub total_samples: Option<u64>,
/// Block size in samples (optional). If None, libFLAC chooses automatically.
/// Typical values: 1152, 2304, 4096.
pub block_size: Option<u32>,
}
@@ -68,6 +115,81 @@ impl Default for EncoderOptions {
}
}
/// Encodes PCM audio data into a FLAC stream.
///
/// This function spawns background tasks to perform the encoding asynchronously.
/// The returned `FlacEncodedStream` implements `AsyncRead` for streaming the FLAC output.
///
/// # Threading Model
///
/// - A Tokio task reads PCM chunks and converts them to i32 samples
/// - A blocking task (via `spawn_blocking`) runs the libFLAC encoder
/// - The encoder's write callback sends encoded data via a channel
/// - Another Tokio task writes FLAC data to an internal duplex stream
///
/// This architecture ensures true streaming: FLAC frames are produced as soon as
/// enough PCM data is available, without waiting for the entire input.
///
/// # Arguments
///
/// * `reader` - Any async reader containing PCM audio in little-endian interleaved format
/// * `format` - Describes the PCM format (sample rate, channels, bit depth)
/// * `options` - Encoding options (compression level, verify, etc.)
///
/// # PCM Input Format
///
/// The PCM data must be:
/// - **Little-endian** byte order
/// - **Interleaved** channels (L, R, L, R for stereo)
/// - **Signed integers** with bit depth matching `format.bits_per_sample`
///
/// # Returns
///
/// A `FlacEncodedStream` that can be read to obtain FLAC-encoded data.
///
/// # Errors
///
/// Returns an error if:
/// - The PCM format is invalid (e.g., unsupported bit depth)
/// - The input stream has incomplete sample data
/// - libFLAC initialization fails
/// - An I/O error occurs
///
/// # Example
///
/// ```no_run
/// use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
/// use tokio::io::AsyncReadExt;
///
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// // Generate 1 second of silence at 44.1kHz stereo 16-bit
/// let sample_rate = 44_100u32;
/// let channels = 2u8;
/// let pcm_data = vec![0u8; sample_rate as usize * channels as usize * 2];
///
/// let format = PcmFormat {
/// sample_rate,
/// channels,
/// bits_per_sample: 16,
/// };
///
/// let options = EncoderOptions {
/// compression_level: 8,
/// total_samples: Some(sample_rate as u64),
/// ..Default::default()
/// };
///
/// let mut stream = encode_flac_stream(&pcm_data[..], format, options).await?;
/// let mut flac_data = Vec::new();
/// stream.read_to_end(&mut flac_data).await?;
/// stream.wait().await?;
///
/// println!("Encoded {} bytes of PCM to {} bytes of FLAC",
/// pcm_data.len(), flac_data.len());
/// # Ok(())
/// # }
/// ```
pub async fn encode_flac_stream<R>(
reader: R,
format: PcmFormat,

View File

@@ -1,3 +1,66 @@
//! # pmoflac
//!
//! Asynchronous FLAC encoding and decoding library for Rust.
//!
//! This library provides streaming FLAC encoding and decoding with a Tokio-based async API.
//! The key feature is **true streaming**: data is processed incrementally without buffering
//! entire files in memory.
//!
//! ## Features
//!
//! - **Async streaming API**: Built on Tokio's `AsyncRead` trait
//! - **Low memory footprint**: Processes data in chunks, not entire files
//! - **Zero-copy where possible**: Efficient buffer management
//! - **Thread-safe**: Uses channels for inter-task communication
//!
//! ## Example: Decode FLAC to PCM
//!
//! ```no_run
//! use pmoflac::decode_flac_stream;
//! use tokio::fs::File;
//! use tokio::io::AsyncReadExt;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let file = File::open("audio.flac").await?;
//! let mut stream = decode_flac_stream(file).await?;
//!
//! let info = stream.info();
//! println!("Sample rate: {} Hz", info.sample_rate);
//! println!("Channels: {}", info.channels);
//!
//! let mut pcm_data = Vec::new();
//! stream.read_to_end(&mut pcm_data).await?;
//! stream.wait().await?;
//!
//! Ok(())
//! }
//! ```
//!
//! ## Example: Encode PCM to FLAC
//!
//! ```no_run
//! use pmoflac::{encode_flac_stream, EncoderOptions, PcmFormat};
//! use tokio::io::AsyncReadExt;
//!
//! #[tokio::main]
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let pcm_data: &[u8] = &[/* PCM samples */];
//! let format = PcmFormat {
//! sample_rate: 44_100,
//! channels: 2,
//! bits_per_sample: 16,
//! };
//!
//! let mut stream = encode_flac_stream(pcm_data, format, EncoderOptions::default()).await?;
//! let mut flac_data = Vec::new();
//! stream.read_to_end(&mut flac_data).await?;
//! stream.wait().await?;
//!
//! Ok(())
//! }
//! ```
pub mod decoder;
pub mod encoder;
pub mod error;

View File

@@ -17,31 +17,87 @@ impl StreamInfo {
}
}
/// Basic PCM format used when encoding to FLAC.
/// Describes the format of PCM audio data.
///
/// This is used when encoding PCM to FLAC to specify the audio properties.
#[derive(Debug, Clone, Copy)]
pub struct PcmFormat {
/// Sample rate in Hz (e.g., 44100, 48000, 96000)
pub sample_rate: u32,
/// Number of audio channels (1 = mono, 2 = stereo, etc.)
pub channels: u8,
/// Bits per sample (typically 16 or 24)
pub bits_per_sample: u8,
}
impl PcmFormat {
/// Validates the PCM format parameters.
///
/// # Errors
///
/// Returns an error if any parameter is invalid or out of the supported range.
///
/// # Warnings
///
/// This function will log warnings (via the error message) for unusual but
/// technically valid configurations.
pub fn validate(&self) -> Result<(), String> {
// Validate channels
if self.channels == 0 {
return Err("channel count must be greater than 0".into());
}
if self.channels > 8 {
return Err("channel count greater than 8 is unsupported".into());
return Err("channel count greater than 8 is unsupported by FLAC".into());
}
// Validate sample rate
if self.sample_rate == 0 {
return Err("sample rate must be greater than 0".into());
}
if self.sample_rate > 655_350 {
return Err("sample rate exceeds FLAC maximum (655350 Hz)".into());
}
// Warn about unusual sample rates
const STANDARD_RATES: &[u32] = &[
8000, 11025, 16000, 22050, 32000, 44100, 48000, 88200, 96000, 176400, 192000, 352800,
384000,
];
if !STANDARD_RATES.contains(&self.sample_rate) {
eprintln!(
"Warning: non-standard sample rate {} Hz (valid but unusual)",
self.sample_rate
);
}
// Validate bit depth
if self.bits_per_sample == 0 || self.bits_per_sample > 32 {
return Err("bits per sample must be in 1..=32".into());
}
// FLAC officially supports 4-32 bits/sample
if self.bits_per_sample < 4 {
eprintln!(
"Warning: bits_per_sample={} is less than 4 (unusual for FLAC)",
self.bits_per_sample
);
}
// Warn about common bit depths
const COMMON_BIT_DEPTHS: &[u8] = &[8, 16, 24, 32];
if !COMMON_BIT_DEPTHS.contains(&self.bits_per_sample) {
eprintln!(
"Warning: non-standard bit depth {} (valid but unusual)",
self.bits_per_sample
);
}
Ok(())
}
/// Returns the number of bytes needed to store one sample at this bit depth.
pub fn bytes_per_sample(&self) -> usize {
bytes_per_sample(self.bits_per_sample)
}

View File

@@ -45,3 +45,154 @@ pub fn le_bytes_to_interleaved_i32(bytes: &[u8], bits_per_sample: u8) -> Result<
Ok(samples)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_roundtrip_16bit() {
let samples = vec![0i32, 1000, -1000, i16::MAX as i32, i16::MIN as i32];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 16, &mut bytes);
assert_eq!(bytes.len(), samples.len() * 2);
let recovered = le_bytes_to_interleaved_i32(&bytes, 16).unwrap();
assert_eq!(recovered, samples);
}
#[test]
fn test_roundtrip_24bit() {
// 24-bit max is 2^23 - 1 = 8388607, min is -2^23 = -8388608
let samples = vec![0i32, 1000, -1000, 8388607, -8388608];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 24, &mut bytes);
assert_eq!(bytes.len(), samples.len() * 3);
let recovered = le_bytes_to_interleaved_i32(&bytes, 24).unwrap();
assert_eq!(recovered, samples);
}
#[test]
fn test_roundtrip_32bit() {
let samples = vec![0i32, 1000, -1000, i32::MAX, i32::MIN];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 32, &mut bytes);
assert_eq!(bytes.len(), samples.len() * 4);
let recovered = le_bytes_to_interleaved_i32(&bytes, 32).unwrap();
assert_eq!(recovered, samples);
}
#[test]
fn test_roundtrip_8bit() {
// 8-bit signed: -128 to 127
let samples = vec![0i32, 100, -100, 127, -128];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 8, &mut bytes);
assert_eq!(bytes.len(), samples.len());
let recovered = le_bytes_to_interleaved_i32(&bytes, 8).unwrap();
assert_eq!(recovered, samples);
}
#[test]
fn test_sign_extension_16bit() {
// Test that sign extension works correctly for 16-bit
let sample = -1i32; // Should be 0xFFFF in 16-bit
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&[sample], 16, &mut bytes);
assert_eq!(bytes.len(), 2);
assert_eq!(bytes[0], 0xFF);
assert_eq!(bytes[1], 0xFF);
let recovered = le_bytes_to_interleaved_i32(&bytes, 16).unwrap();
assert_eq!(recovered[0], -1);
}
#[test]
fn test_sign_extension_24bit() {
// Test that sign extension works correctly for 24-bit
let sample = -1i32;
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&[sample], 24, &mut bytes);
assert_eq!(bytes.len(), 3);
assert_eq!(bytes[0], 0xFF);
assert_eq!(bytes[1], 0xFF);
assert_eq!(bytes[2], 0xFF);
let recovered = le_bytes_to_interleaved_i32(&bytes, 24).unwrap();
assert_eq!(recovered[0], -1);
}
#[test]
fn test_misaligned_bytes_error() {
// 16-bit samples need even number of bytes
let bytes = vec![0, 1, 2]; // 3 bytes, not aligned to 2
let result = le_bytes_to_interleaved_i32(&bytes, 16);
assert!(result.is_err());
assert!(result
.unwrap_err()
.contains("not aligned to 2 bytes/sample"));
}
#[test]
fn test_empty_samples() {
let samples: Vec<i32> = vec![];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 16, &mut bytes);
assert_eq!(bytes.len(), 0);
let recovered = le_bytes_to_interleaved_i32(&bytes, 16).unwrap();
assert_eq!(recovered.len(), 0);
}
#[test]
fn test_stereo_interleaved_16bit() {
// Simulate stereo: L, R, L, R
let samples = vec![1000i32, 2000, 3000, 4000];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, 16, &mut bytes);
assert_eq!(bytes.len(), 8); // 4 samples * 2 bytes
let recovered = le_bytes_to_interleaved_i32(&bytes, 16).unwrap();
assert_eq!(recovered, samples);
}
#[test]
fn test_value_truncation_overflow() {
// Test that values outside the valid range for a bit depth
// are properly truncated via sign extension
let huge_value = i32::MAX; // Way beyond 16-bit range
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&[huge_value], 16, &mut bytes);
let recovered = le_bytes_to_interleaved_i32(&bytes, 16).unwrap();
// The value should be truncated to 16-bit and sign-extended
assert_eq!(recovered[0], -1); // 0xFFFF sign-extended
}
#[test]
fn test_multiple_bit_depths() {
for bits in [8, 16, 24, 32] {
let samples = vec![0i32, 100, -100];
let mut bytes = Vec::new();
interleaved_i32_to_le_bytes(&samples, bits, &mut bytes);
let expected_bytes = samples.len() * bytes_per_sample(bits);
assert_eq!(bytes.len(), expected_bytes);
let recovered = le_bytes_to_interleaved_i32(&bytes, bits).unwrap();
assert_eq!(recovered, samples);
}
}
}