diff --git a/crates/audio/README.md b/crates/audio/README.md index e05ae3e7c..3f321821d 100644 --- a/crates/audio/README.md +++ b/crates/audio/README.md @@ -31,7 +31,7 @@ No default features. The shared types (`AudioBuffer`, `AudioInfo`, `ChannelLayou | `encode` | every encoder: the WAV, AIFF and FLAC file writers, and the AAC, ALAC and AC-3/E-AC-3 packet encoders | `lossless-encode`, `aac-encode`, `alac-encode`, `ac3-encode`, `eac3-encode` | | `lossless-encode` | WAV, AIFF and FLAC writing | `pcm-encode`, `flac-encode` | | `pcm-encode` | `encode`, `FileEncoder` for WAV and AIFF; `wav_float_header` | none | -| `flac-encode` | `encode`, `FileEncoder` for FLAC | craft-media `bitstream` | +| `flac-encode` | `encode`, `FileEncoder` for FLAC; `flac::Encoder`, the packet encoder for callers that mux FLAC themselves | craft-media `bitstream` | | `aac-decode`, `aac-encode` | `aac::Decoder`, `aac::Encoder` | craft-media `bitstream` | | `alac-decode` | `alac::Decoder` | none | | `alac-encode` | `alac::Encoder` | none | diff --git a/crates/audio/src/lib.rs b/crates/audio/src/lib.rs index 4285ca105..cac8f0291 100644 --- a/crates/audio/src/lib.rs +++ b/crates/audio/src/lib.rs @@ -22,7 +22,7 @@ //! - [`peaks`] (feature `peaks`): min/max/RMS pyramid for waveform drawing. //! - Codec modules for packet-level use, each with its own features: [`aac`] (`aac-decode`, //! `aac-encode`), [`alac`] (`alac-decode`, `alac-encode`), [`opus`] (`opus-decode`), [`ac3`] -//! (`ac3-decode`), [`mpa`] (`mpa-decode`). +//! (`ac3-decode`), [`mpa`] (`mpa-decode`), [`flac`] (`flac-encode`: the packet encoder). //! //! The shared types below build with no features. @@ -51,6 +51,30 @@ mod wav; #[cfg(any(feature = "pcm-encode", feature = "flac-encode"))] mod write; +/// The FLAC packet encoder (RFC 9639), for a caller that muxes FLAC itself (into MP4, Matroska or +/// its own `.flac` writer): planar integer samples in, frames out, and the STREAMINFO to patch in +/// at the end. [`FileEncoder`] and [`encode`] write whole `.flac` files from `f32` instead. +/// +/// ``` +/// use craft_audio::flac::{Encoder, EncoderConfig, STREAMINFO_OFFSET}; +/// +/// let left: Vec = (0..48_000).map(|i| ((i as f64 * 0.05).sin() * 20_000.0) as i32).collect(); +/// let right = left.clone(); +/// let mut enc = Encoder::new(EncoderConfig::new(48_000, 2, 16))?; +/// let mut file = enc.header(&[]); +/// file.extend(enc.encode(&[&left, &right])?); +/// file.extend(enc.finish()); +/// let at = STREAMINFO_OFFSET as usize; +/// file[at..at + 34].copy_from_slice(&enc.streaminfo()); +/// assert_eq!(&file[..4], b"fLaC"); +/// assert!(file.len() < 48_000 * 2 * 2 / 2); // well under half of 16-bit PCM +/// # Ok::<(), craft_audio::flac::Error>(()) +/// ``` +#[cfg(feature = "flac-encode")] +pub mod flac { + pub use crate::write::flac::encoder::{Encoder, EncoderConfig, Error, STREAMINFO_OFFSET}; +} + pub use buffer::AudioBuffer; /// The craft-media this crate is built with, for its containers: using it rather than a separate /// dependency keeps the types the two crates share identical. @@ -135,6 +159,13 @@ impl From for AudioError { } } +#[cfg(feature = "flac-encode")] +impl From for AudioError { + fn from(e: flac::Error) -> Self { + AudioError::Encode(format!("FLAC: {e}")) + } +} + #[cfg(feature = "mpa-decode")] impl From for AudioError { fn from(e: mpa::Error) -> Self { diff --git a/crates/audio/src/write/flac/encoder.rs b/crates/audio/src/write/flac/encoder.rs index 02a22a0c0..a7a453d20 100644 --- a/crates/audio/src/write/flac/encoder.rs +++ b/crates/audio/src/write/flac/encoder.rs @@ -13,18 +13,30 @@ use super::predict::{Effort, Subframe}; /// Byte offset of the 34-byte STREAMINFO body in [`Encoder::header`]. pub const STREAMINFO_OFFSET: u64 = 8; +/// Why the encoder refused a configuration or a block of samples. +#[non_exhaustive] #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] pub enum Error { + /// The configuration is outside what FLAC can code; the text names the field. #[error("invalid configuration: {0}")] InvalidConfig(&'static str), + /// A block had a different number of channels than the configuration. #[error("expected {expected} channels, got {got}")] - Channels { expected: usize, got: usize }, + Channels { + /// The configured channel count. + expected: usize, + /// The channels in the block. + got: usize, + }, + /// The channels of a block differ in length. #[error("channels of unequal length")] UnequalChannels, + /// A sample is outside the configured sample size. #[error("sample {0} does not fit the configured sample size")] SampleRange(i32), } +/// What to encode and how hard to try. #[derive(Clone, Debug)] pub struct EncoderConfig { /// 1 to 1 048 575 Hz. @@ -41,11 +53,14 @@ pub struct EncoderConfig { } impl EncoderConfig { + /// 4096-sample blocks at level 5. pub fn new(sample_rate: u32, channels: u8, bits_per_sample: u8) -> Self { EncoderConfig { sample_rate, channels, bits_per_sample, block_size: 4096, level: 5 } } } +/// The packet encoder: [`Encoder::header`], then frames from [`Encoder::encode`] and +/// [`Encoder::finish`], then [`Encoder::streaminfo`] over [`STREAMINFO_OFFSET`]. pub struct Encoder { cfg: EncoderConfig, effort: Effort, @@ -61,6 +76,7 @@ pub struct Encoder { } impl Encoder { + /// An encoder for `cfg`, or [`Error::InvalidConfig`] when FLAC cannot code it. pub fn new(cfg: EncoderConfig) -> Result { if !(1..=8).contains(&cfg.channels) { return Err(Error::InvalidConfig("1 to 8 channels")); @@ -174,6 +190,12 @@ impl Encoder { Ok(out) } + /// Samples per channel waiting for a full block: the length of the frame [`Encoder::finish`] + /// would write. + pub fn pending_samples(&self) -> usize { + self.pending.first().map_or(0, Vec::len) + } + /// Encode what is left as a final, shorter frame. pub fn finish(&mut self) -> Vec { if self.pending.first().is_none_or(Vec::is_empty) { diff --git a/crates/audio/src/write/flac/mod.rs b/crates/audio/src/write/flac/mod.rs index f2674d427..6f562e6e2 100644 --- a/crates/audio/src/write/flac/mod.rs +++ b/crates/audio/src/write/flac/mod.rs @@ -2,7 +2,7 @@ //! (linear prediction, stereo decorrelation, STREAMINFO MD5) into any `Write + Seek`. mod crc; -mod encoder; +pub(crate) mod encoder; mod frame; mod md5; mod predict; @@ -49,7 +49,7 @@ fn mask_comment(channels: usize, mask: Option) -> Option { } fn encoder_error(e: encoder::Error) -> AudioError { - AudioError::Encode(format!("FLAC: {e}")) + e.into() } /// A FLAC stream written block by block; [`Writer::finish`] fills in the STREAMINFO sizes, sample diff --git a/crates/audio/src/write/mod.rs b/crates/audio/src/write/mod.rs index eaad8e961..25c9b2528 100644 --- a/crates/audio/src/write/mod.rs +++ b/crates/audio/src/write/mod.rs @@ -4,7 +4,7 @@ #[cfg(feature = "pcm-encode")] mod aiff; #[cfg(feature = "flac-encode")] -mod flac; +pub(crate) mod flac; mod pcm; #[cfg(feature = "pcm-encode")] mod wav;