Files
craft-libs/crates/audio/src/lib.rs
T
Aleksander Helgaker 493476eb8d craft-audio: native MP3 (Layer III) decoder (feature mp3-decode)
* craft-audio: native MP3 (Layer III) decoder (feature `mp3-decode`)

A clean-room Layer III decoder written from ISO/IEC 11172-3 and 13818-3:
MPEG-1, MPEG-2 and MPEG-2.5 (8-48 kHz), CBR and VBR, mono, stereo, dual
channel and joint stereo (mid/side and intensity), long, short and mixed
blocks, and the bit reservoir. It shares the encoder's Huffman and
scalefactor band tables and the MP1/MP2 synthesis filter.

`decode`, `Decoder` and `TrackDecoder` now read raw .mp3 files and MP3 in
MP4 and Matroska natively instead of through symphonia. A Xing / Info / VBRI
frame is not a packet, and the LAME tag's encoder delay and padding are
applied, so a file decodes to its exact presented length. The seek
pre-roll covers how far back the bit reservoir can reach, so a seek is
bit-exact. symphonia stays the fallback for free-format MP3.

The encoder moves to mp3/encoder.rs so that each half builds without the
other.

* craft-audio: MP3 review and CI fixes

- Huffman: big values stop at the granule's part2_3_length. A damaged
  granule no longer reads the next granule's bits, as ffmpeg does.
- Joint stereo runs on the lines in coded order, before short blocks are
  reordered, as ffmpeg and libmad do. Where the two channels' block layouts
  differ, against the specification, each channel is then reordered by its
  own layout.
- Seek pre-roll: sized from the smallest frame the stream can hold (the
  lowest bit rate for its version, rate, mode and CRC), not the last frame,
  so a seek into a VBR stream's small frames still has the reservoir. The
  decoder learns the stream's format from its first frame.
- Mixed-block oracle test: edit only granules whose scalefactors take no
  bits, so the edited streams stay valid, and not before a pure short
  block. ffmpeg's short-block IMDCT drops overlap that a mixed block's long
  subbands carry, where the specification adds it. Use 16 kHz instead of
  12 kHz.
- Routing oracle: ffmpeg before 9 decodes the 1105 samples of the MP3
  Matroska CodecDelay.

* craft-audio: MPEG audio packets of several frames keep their length

An MP4 or Matroska packet may hold several MP1/MP2/MP3 frames.
TrackDecoder now counts the frames each packet holds from their headers:

- frames_per_packet is None once a packet in the lookahead or a decoded
  packet has held several frames, so an MP4 track's length is no longer
  capped at one frame per sample and the container's timing decides.
- Concealment is the length of the frames a damaged packet holds. For MP3
  a packet lost whole is as long as the last packet, and one frame before
  any is decoded.

Also corrects the Huffman doc comment: a big-value pair that starts
before the granule's end is kept whole, as ffmpeg does.

* craft-audio: MPEG audio concealment after partial packets and before the first

- A packet whose later frame fails decodes only in part: like AC-3, it no
  longer replaces the length of the last whole packet that concealment
  uses.
- MP1/MP2: a packet lost before any has decoded, or right after a seek, is
  one frame (384 or 1152 samples) of silence rather than none, as MP3's
  already is.

---------

Co-authored-by: alehel <git@ornumhelgaker.no>
2026-11-12 09:12:40 -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/MP3 and PCM in MP4/MOV, Matroska/WebM, Ogg or raw streams go through craft-media's
//! demuxers and [`TrackDecoder`]; everything else (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-decode`, `mp3-encode`),
//! [`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(any(feature = "mp3-encode", feature = "mp3-decode"))]
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}");
}
}