Files
craft-libs/crates/audio/src/lib.rs
T
Dylan M. Taylor 119f6e252e craft-audio: integer PCM of WAV and AIFF files bit for bit, and minimal files around it (#917)
wav_pcm and aiff_pcm (feature decode-native) return a file's integer PCM as stored
(RawPcm: little-endian interleaved bytes, 8-bit unsigned, with channels, rate and
bits), built on the existing WAV and AIFF chunk parsers. pcm_wav_file,
pcm_wav_header, pcm_aiff_file and pcm_aiff_header (feature pcm-encode) write the
44-byte canonical WAVE header and the plain AIFF header around such bytes.

For essence embedded in AAF and OMF files, which is moved bit for bit and whose
descriptors hold a bare header as a summary. The writers are FilmCraft's
(filmcraft-interchange wav.rs); the readers replace its second WAV/AIFF parser
with this crate's.
2026-11-11 18:52:13 -05:00

496 lines
18 KiB
Rust

#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![warn(missing_docs)]
// The docs link items of every feature; built with fewer features, those links have no target.
// CI and docs.rs build the docs with all features, where every link must resolve.
#![cfg_attr(not(all(feature = "decode", feature = "encode", feature = "peaks")), allow(rustdoc::broken_intra_doc_links))]
//! Audio file I/O and codecs for the Craft apps. Input and output are byte slices or a
//! [`craft_media::ByteSource`] (no filesystem access of its own); builds for
//! `wasm32-unknown-unknown`.
//!
//! - [`decode`], [`probe`], [`Decoder`] (streaming, seekable), [`tracks`], [`detect_container`]
//! (feature `decode`): WAV and AIFF are read natively; AAC, ALAC, Opus, AC-3/E-AC-3, MP1/MP2 and
//! PCM in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's demuxers and
//! [`TrackDecoder`]; everything else (MP3, FLAC, Vorbis, CAF, ADPCM, A-law, µ-law) through
//! `symphonia`, which is to be replaced by native decoders. Feature `decode-native` is the
//! same without symphonia.
//! - [`TrackDecoder`] (any decoder feature): one track's packets from any demuxer to planar
//! `f32` in WAV channel order.
//! - [`encode`], [`FileEncoder`]: WAV/BWF/RF64 and AIFF/AIFF-C (feature `pcm-encode`, which also
//! gives [`wav_float_header`]) and FLAC (feature `flac-encode`), whole-buffer or incremental into
//! any `Write + Seek`. Feature `lossless-encode` turns on both; feature `encode` turns on
//! `lossless-encode` and every codec's encoder.
//! - [`wav_pcm`], [`aiff_pcm`] (feature `decode-native`) and [`pcm_wav_file`], [`pcm_wav_header`],
//! [`pcm_aiff_file`], [`pcm_aiff_header`] (feature `pcm-encode`): the integer PCM of a WAV or AIFF
//! file bit for bit, and minimal files around such PCM, for essence embedded in other containers.
//! - [`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`), [`mp3`] (`mp3-encode`: the Layer III encoder),
//! [`flac`] (`flac-encode`: the packet encoder).
//!
//! The shared types below build with no features.
#[cfg(any(feature = "aac-decode", feature = "aac-encode"))]
pub mod aac;
#[cfg(any(feature = "ac3-decode", feature = "ac3-encode", feature = "eac3-encode"))]
pub mod ac3;
#[cfg(any(feature = "alac-decode", feature = "alac-encode"))]
pub mod alac;
mod buffer;
mod layout;
#[cfg(feature = "mp3-encode")]
pub mod mp3;
#[cfg(feature = "mpa-decode")]
pub mod mpa;
#[cfg(feature = "opus-decode")]
pub mod opus;
#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))]
mod pcm;
#[cfg(feature = "peaks")]
pub mod peaks;
#[cfg(feature = "decode-native")]
mod read;
#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))]
mod track;
#[cfg(any(feature = "decode-native", feature = "pcm-encode"))]
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<i32> = (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.
pub use craft_media;
pub use craft_media::{ByteSource, CodecId, CodecParams, Container, PcmFormat, ReadSeekSource};
pub use layout::{ChannelLayout, Speaker};
#[cfg(feature = "decode-native")]
pub use read::SharedSource;
#[cfg(feature = "decode-native")]
pub use read::{
DECODE_EXTENSIONS, DEFAULT_MAX_SAMPLES, DecodeOptions, Decoder, TrackInfo, decode, decode_with, detect_container, probe, probe_with, tracks,
tracks_of,
};
#[cfg(feature = "decode-native")]
pub use read::{RawPcm, aiff_pcm, wav_pcm};
#[cfg(any(feature = "aac-decode", feature = "alac-decode", feature = "opus-decode", feature = "ac3-decode", feature = "mpa-decode"))]
pub use track::{LOOKAHEAD_PACKETS, TrackDecoder};
#[cfg(feature = "pcm-encode")]
pub use write::wav_float_header;
#[cfg(any(feature = "pcm-encode", feature = "flac-encode"))]
pub use write::{ENCODE_EXTENSIONS, FileEncoder, encode, encode_format_for};
#[cfg(feature = "pcm-encode")]
pub use write::{pcm_aiff_file, pcm_aiff_header, pcm_wav_file, pcm_wav_header};
/// Errors from probing, decoding and encoding.
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum AudioError {
/// The format, codec or option is valid but not supported.
#[error("unsupported audio: {0}")]
Unsupported(String),
/// The input is corrupt or not what it claims to be.
#[error("malformed audio: {0}")]
Malformed(String),
/// The input (or requested output) exceeds a size limit.
#[error("audio too large: {0}")]
TooLarge(String),
/// Encoding failed (e.g. an empty buffer).
#[error("audio encode failed: {0}")]
Encode(String),
/// Reading the input or writing the output failed (a source that stopped serving reads, a
/// full disk). Bytes missing because the input is truncated are [`AudioError::Malformed`].
#[error("audio I/O failed: {0}")]
Io(#[from] std::io::Error),
}
/// Result alias for this module.
pub type Result<T> = std::result::Result<T, AudioError>;
// Codec errors convert into `AudioError`, so code mixing the file API with the codec modules can
// use `?` throughout: a codec's "unsupported" stays `Unsupported`, anything else in the input is
// `Malformed`, and an encoder's refusal is `Encode`.
#[cfg(any(feature = "aac-decode", feature = "aac-encode"))]
impl From<aac::Error> for AudioError {
fn from(e: aac::Error) -> Self {
match e {
aac::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
aac::Error::InvalidSettings(_) => AudioError::Encode(e.to_string()),
_ => AudioError::Malformed(e.to_string()),
}
}
}
#[cfg(feature = "opus-decode")]
impl From<opus::Error> for AudioError {
fn from(e: opus::Error) -> Self {
match e {
opus::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
_ => AudioError::Malformed(e.to_string()),
}
}
}
#[cfg(feature = "ac3-decode")]
impl From<ac3::Error> for AudioError {
fn from(e: ac3::Error) -> Self {
match e {
ac3::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
_ => AudioError::Malformed(e.to_string()),
}
}
}
#[cfg(any(feature = "ac3-encode", feature = "eac3-encode"))]
impl From<ac3::encoder::Error> for AudioError {
fn from(e: ac3::encoder::Error) -> Self {
AudioError::Encode(e.to_string())
}
}
#[cfg(feature = "mp3-encode")]
impl From<mp3::Error> for AudioError {
fn from(e: mp3::Error) -> Self {
AudioError::Encode(e.to_string())
}
}
#[cfg(feature = "flac-encode")]
impl From<flac::Error> for AudioError {
fn from(e: flac::Error) -> Self {
AudioError::Encode(format!("FLAC: {e}"))
}
}
#[cfg(feature = "mpa-decode")]
impl From<mpa::Error> for AudioError {
fn from(e: mpa::Error) -> Self {
match e {
mpa::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
_ => AudioError::Malformed(e.to_string()),
}
}
}
#[cfg(any(feature = "alac-decode", feature = "alac-encode"))]
impl From<alac::Error> for AudioError {
fn from(e: alac::Error) -> Self {
match e {
alac::Error::Unsupported(_) => AudioError::Unsupported(e.to_string()),
alac::Error::InvalidInput(_) => AudioError::Encode(e.to_string()),
_ => AudioError::Malformed(e.to_string()),
}
}
}
/// The file formats [`encode`] writes. Each needs its feature: WAV and AIFF `pcm-encode`, FLAC
/// `flac-encode`; asking for one that is not built returns [`AudioError::Unsupported`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum EncodeFormat {
/// WAV / BWF, RF64 above 4 GiB.
Wav,
/// AIFF (integer depths) or AIFF-C `fl32` (float).
Aiff,
/// FLAC (integer depths).
Flac,
}
impl EncodeFormat {
/// Every format, in the order menus list them. Whether [`encode`] writes one depends on its
/// feature.
pub const ALL: &[EncodeFormat] = &[EncodeFormat::Wav, EncodeFormat::Aiff, EncodeFormat::Flac];
/// The extension a new file of this format gets, lower case and without the dot: `"wav"`,
/// `"aif"`, `"flac"`. [`encode_format_for`] maps it back.
pub fn extension(self) -> &'static str {
match self {
EncodeFormat::Wav => "wav",
EncodeFormat::Aiff => "aif",
EncodeFormat::Flac => "flac",
}
}
/// The media type a file of this format is served or downloaded as.
pub fn mime_type(self) -> &'static str {
match self {
EncodeFormat::Wav => "audio/wav",
EncodeFormat::Aiff => "audio/aiff",
EncodeFormat::Flac => "audio/flac",
}
}
/// The container this format writes.
pub fn container(self) -> Container {
match self {
EncodeFormat::Wav => Container::Wav,
EncodeFormat::Aiff => Container::Aiff,
EncodeFormat::Flac => Container::Flac,
}
}
}
/// Stored sample representation.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum SampleFormat {
/// Integer PCM, 1-8 bits.
Int8,
/// Integer PCM, 9-16 bits.
Int16,
/// Integer PCM, 17-24 bits.
Int24,
/// Integer PCM, 25-32 bits.
Int32,
/// 32-bit IEEE float.
Float32,
/// 64-bit IEEE float.
Float64,
/// A lossy codec (MP3, AAC, Vorbis, ...): no fixed bit depth.
Compressed,
}
/// Broadcast Wave (`bext`) metadata.
///
/// `#[non_exhaustive]`: build it with `BwfInfo::default()` and the `with_*` methods.
#[non_exhaustive]
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BwfInfo {
/// `Description` (up to 256 bytes).
pub description: String,
/// `Originator` (up to 32 bytes).
pub originator: String,
/// `OriginatorReference` (up to 32 bytes).
pub originator_reference: String,
/// `yyyy-mm-dd`.
pub origination_date: String,
/// `hh:mm:ss`.
pub origination_time: String,
/// Sample count since midnight of the first sample (the timestamp Pro Tools spots to).
pub time_reference: u64,
}
impl BwfInfo {
/// Sets `description`.
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = description.into();
self
}
/// Sets `originator`.
pub fn with_originator(mut self, originator: impl Into<String>) -> Self {
self.originator = originator.into();
self
}
/// Sets `originator_reference`.
pub fn with_originator_reference(mut self, reference: impl Into<String>) -> Self {
self.originator_reference = reference.into();
self
}
/// `date` as `yyyy-mm-dd`, `time` as `hh:mm:ss`.
pub fn with_origination(mut self, date: impl Into<String>, time: impl Into<String>) -> Self {
(self.origination_date, self.origination_time) = (date.into(), time.into());
self
}
/// Sets `time_reference`.
pub fn with_time_reference(mut self, time_reference: u64) -> Self {
self.time_reference = time_reference;
self
}
}
/// What `probe` learns about a file without decoding all of it.
///
/// `#[non_exhaustive]`: fields may be added, so it is only built by this crate.
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AudioInfo {
/// The file format around the audio.
pub container: Container,
/// The codec of the audio track ([`CodecId::Pcm`] for uncompressed audio).
pub codec: CodecId,
/// The stored sample representation.
pub sample_format: SampleFormat,
/// The decoded sample rate (for HE-AAC, the rate after spectral band replication).
pub sample_rate: u32,
/// Decoded channel count.
pub channels: u16,
/// Which speaker each decoded channel feeds. Decoded audio is in WAV channel order when the
/// layout is ordered.
pub layout: ChannelLayout,
/// Length in frames (samples per channel), after container trimming.
pub frames: u64,
/// Broadcast Wave metadata from a WAV `bext` chunk.
pub bwf: Option<BwfInfo>,
/// Packets that were damaged or failed to decode and were replaced by concealment (silence of
/// their length), and runs of FLAC frames lost to a failed checksum, counted as audio is decoded: by [`decode`] for the whole file, and so far by
/// a [`Decoder`]. 0 from [`probe`] when it read only headers.
pub damaged: u64,
}
impl AudioInfo {
/// Info for a stream with `layout`'s channels and no BWF metadata.
#[cfg(feature = "decode-native")]
pub(crate) fn new(
container: Container,
codec: CodecId,
sample_format: SampleFormat,
sample_rate: u32,
layout: ChannelLayout,
frames: u64,
) -> AudioInfo {
let channels = layout.channels();
AudioInfo { container, codec, sample_format, sample_rate, channels, layout, frames, bwf: None, damaged: 0 }
}
/// Duration in seconds.
pub fn duration_secs(&self) -> f64 {
if self.sample_rate == 0 { 0.0 } else { self.frames as f64 / f64::from(self.sample_rate) }
}
}
/// Bit depth for `encode`.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum BitDepth {
/// 16-bit integer.
Int16,
/// 24-bit integer.
Int24,
/// 32-bit integer.
Int32,
/// Preserve finite samples, including values above full scale; map NaN to 0 and infinities to ±1.
Float32,
}
/// Options for `encode` and `FileEncoder`.
///
/// `#[non_exhaustive]`: build it with [`EncodeOptions::new`] and the `with_*` methods.
///
/// ```
/// use craft_audio::{BitDepth, EncodeFormat, EncodeOptions};
///
/// let opts = EncodeOptions::new(EncodeFormat::Flac).with_bit_depth(BitDepth::Int16).with_dither(true);
/// assert_eq!(opts.format, EncodeFormat::Flac);
/// ```
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct EncodeOptions {
/// Output file format.
pub format: EncodeFormat,
/// Sample depth to write.
pub bit_depth: BitDepth,
/// Add TPDF dither when quantizing to an integer depth.
pub dither: bool,
/// Write a BWF `bext` chunk (WAV only; ignored for other formats).
pub bwf: Option<BwfInfo>,
/// The speaker layout to record, for layouts a channel count alone cannot identify (5.1 with
/// side vs back surrounds, 7.0 vs 6.1, Ambisonics as [`ChannelLayout::unordered`]). `None` is
/// the usual layout for the channel count. WAV stores it as `dwChannelMask`, FLAC as a
/// `WAVEFORMATEXTENSIBLE_CHANNEL_MASK` comment when decoders would assume another layout for
/// the count (they read 5 and 6 channels with side surrounds and 7 as 6.1 with sides, so the
/// usual layouts for those counts get one; see the README's limits); AIFF has a fixed channel
/// order and ignores it. It must have as many channels as the audio.
pub layout: Option<ChannelLayout>,
/// Write WAV in the RF64 layout even below 4 GiB (WAV only; ignored for other formats).
pub force_rf64: bool,
}
impl EncodeOptions {
/// Options for `format`: 24-bit, no dither, no BWF chunk, the usual layout.
pub fn new(format: EncodeFormat) -> Self {
Self { format, bit_depth: BitDepth::Int24, dither: false, bwf: None, layout: None, force_rf64: false }
}
/// Sets `format`.
pub fn with_format(mut self, format: EncodeFormat) -> Self {
self.format = format;
self
}
/// Sets `bit_depth`.
pub fn with_bit_depth(mut self, bit_depth: BitDepth) -> Self {
self.bit_depth = bit_depth;
self
}
/// Sets `dither`.
pub fn with_dither(mut self, dither: bool) -> Self {
self.dither = dither;
self
}
/// Sets `bwf`.
pub fn with_bwf(mut self, bwf: BwfInfo) -> Self {
self.bwf = Some(bwf);
self
}
/// Sets `layout`.
pub fn with_layout(mut self, layout: ChannelLayout) -> Self {
self.layout = Some(layout);
self
}
/// Sets `force_rf64`.
pub fn with_force_rf64(mut self, force_rf64: bool) -> Self {
self.force_rf64 = force_rf64;
self
}
}
impl Default for EncodeOptions {
/// 24-bit WAV.
fn default() -> Self {
Self::new(EncodeFormat::Wav)
}
}
// An encoder refusing its settings is an encode error, not damaged input.
#[cfg(all(test, feature = "aac-encode"))]
mod tests {
use super::*;
#[test]
fn aac_encoder_settings_are_encode_errors() {
let e = match aac::Encoder::new(aac::EncoderConfig::cbr(44_000, 2, 128_000)) {
Err(e) => AudioError::from(e),
Ok(_) => panic!("44 kHz is not an AAC rate"),
};
assert!(matches!(e, AudioError::Encode(_)), "{e}");
}
}