#![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 = (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 = std::result::Result; // 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 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 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 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 for AudioError { fn from(e: ac3::encoder::Error) -> Self { AudioError::Encode(e.to_string()) } } #[cfg(feature = "mp3-encode")] impl From for AudioError { fn from(e: mp3::Error) -> Self { AudioError::Encode(e.to_string()) } } #[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 { 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 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) -> Self { self.description = description.into(); self } /// Sets `originator`. pub fn with_originator(mut self, originator: impl Into) -> Self { self.originator = originator.into(); self } /// Sets `originator_reference`. pub fn with_originator_reference(mut self, reference: impl Into) -> 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, time: impl Into) -> 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, /// 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, /// 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, /// 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}"); } }