diff --git a/crates/pile-audio/Cargo.toml b/crates/pile-audio/Cargo.toml new file mode 100644 index 0000000..738bc6c --- /dev/null +++ b/crates/pile-audio/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "pile-audio" +version = { workspace = true } +rust-version = { workspace = true } +edition = { workspace = true } + +[lints] +workspace = true + +[dependencies] +thiserror = { workspace = true } +mime = { workspace = true } +base64 = { workspace = true } +strum = { workspace = true } +smartstring = { workspace = true } + +[dev-dependencies] +paste = { workspace = true } +rand = { workspace = true } +sha2 = { workspace = true } +itertools = { workspace = true } diff --git a/crates/pile-audio/src/common/mod.rs b/crates/pile-audio/src/common/mod.rs new file mode 100644 index 0000000..7782dcf --- /dev/null +++ b/crates/pile-audio/src/common/mod.rs @@ -0,0 +1,5 @@ +//! Components shared between many different formats + +pub mod picturetype; +pub mod tagtype; +pub mod vorbiscomment; diff --git a/crates/pile-audio/src/common/picturetype.rs b/crates/pile-audio/src/common/picturetype.rs new file mode 100644 index 0000000..a760b32 --- /dev/null +++ b/crates/pile-audio/src/common/picturetype.rs @@ -0,0 +1,102 @@ +//! An audio picture type, according to the ID3v2 APIC frame + +use std::fmt::Display; + +/// We failed to decode a picture type +#[derive(Debug)] +pub struct PictureTypeError { + idx: u32, +} + +impl Display for PictureTypeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Bad picture type `{}`", self.idx) + } +} + +impl std::error::Error for PictureTypeError {} + +/// A picture type according to the ID3v2 APIC frame +#[allow(missing_docs)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum PictureType { + Other, + PngFileIcon, + OtherFileIcon, + FrontCover, + BackCover, + LeafletPage, + Media, + LeadArtist, + Artist, + Conductor, + BandOrchestra, + Composer, + Lyricist, + RecLocation, + DuringRecording, + DuringPerformance, + VideoScreenCapture, + ABrightColoredFish, + Illustration, + ArtistLogotype, + PublisherLogotype, +} + +impl PictureType { + /// Try to decode a picture type from the given integer. + /// Returns an error if `idx` is invalid. + pub fn from_idx(idx: u32) -> Result { + Ok(match idx { + 0 => PictureType::Other, + 1 => PictureType::PngFileIcon, + 2 => PictureType::OtherFileIcon, + 3 => PictureType::FrontCover, + 4 => PictureType::BackCover, + 5 => PictureType::LeafletPage, + 6 => PictureType::Media, + 7 => PictureType::LeadArtist, + 8 => PictureType::Artist, + 9 => PictureType::Conductor, + 10 => PictureType::BandOrchestra, + 11 => PictureType::Composer, + 12 => PictureType::Lyricist, + 13 => PictureType::RecLocation, + 14 => PictureType::DuringRecording, + 15 => PictureType::DuringPerformance, + 16 => PictureType::VideoScreenCapture, + 17 => PictureType::ABrightColoredFish, + 18 => PictureType::Illustration, + 19 => PictureType::ArtistLogotype, + 20 => PictureType::PublisherLogotype, + _ => return Err(PictureTypeError { idx }), + }) + } + + /// Return the index of this picture type + pub fn to_idx(&self) -> u32 { + match self { + PictureType::Other => 0, + PictureType::PngFileIcon => 1, + PictureType::OtherFileIcon => 2, + PictureType::FrontCover => 3, + PictureType::BackCover => 4, + PictureType::LeafletPage => 5, + PictureType::Media => 6, + PictureType::LeadArtist => 7, + PictureType::Artist => 8, + PictureType::Conductor => 9, + PictureType::BandOrchestra => 10, + PictureType::Composer => 11, + PictureType::Lyricist => 12, + PictureType::RecLocation => 13, + PictureType::DuringRecording => 14, + PictureType::DuringPerformance => 15, + PictureType::VideoScreenCapture => 16, + PictureType::ABrightColoredFish => 17, + PictureType::Illustration => 18, + PictureType::ArtistLogotype => 19, + PictureType::PublisherLogotype => 20, + } + } +} diff --git a/crates/pile-audio/src/common/tagtype.rs b/crates/pile-audio/src/common/tagtype.rs new file mode 100644 index 0000000..2ce04ea --- /dev/null +++ b/crates/pile-audio/src/common/tagtype.rs @@ -0,0 +1,40 @@ +//! Cross-format normalized tag types +use smartstring::{LazyCompact, SmartString}; +use strum::{Display, EnumString}; + +/// A universal tag type +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Clone, EnumString, Display)] +pub enum TagType { + /// A tag we didn't recognize + Other(SmartString), + + /// Album name + Album, + /// Album artist + AlbumArtist, + /// Comment + Comment, + /// Release date + ReleaseDate, + /// Disk number + DiskNumber, + /// Total disks in album + DiskTotal, + /// Genre + Genre, + /// International standard recording code + Isrc, + /// Track lyrics, possibly time-coded + Lyrics, + /// This track's number in its album + TrackNumber, + /// The total number of tracks in this track's album + TrackTotal, + /// The title of this track + TrackTitle, + /// This track's artist (the usual `Artist`, + /// compare to `AlbumArtist`) + TrackArtist, + /// The year this track was released + Year, +} diff --git a/crates/pile-audio/src/common/vorbiscomment.rs b/crates/pile-audio/src/common/vorbiscomment.rs new file mode 100644 index 0000000..d3bb04a --- /dev/null +++ b/crates/pile-audio/src/common/vorbiscomment.rs @@ -0,0 +1,327 @@ +//! Decode and write Vorbis comment blocks + +use base64::Engine; +use smartstring::{LazyCompact, SmartString}; +use std::{ + fmt::Display, + io::{Cursor, Read, Write}, + string::FromUtf8Error, +}; + +use crate::flac::blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock}; + +use super::tagtype::TagType; + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum VorbisCommentDecodeError { + /// We encountered an IoError while processing a block + IoError(std::io::Error), + + /// We tried to decode a string, but got invalid data + FailedStringDecode(FromUtf8Error), + + /// The given comment string isn't within spec + MalformedCommentString(String), + + /// The comment we're reading is invalid + MalformedData, + + /// We tried to decode picture data, but it was malformed. + MalformedPicture, +} + +impl Display for VorbisCommentDecodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(_) => write!(f, "io error while reading vorbis comments"), + Self::FailedStringDecode(_) => { + write!(f, "string decode error while reading vorbis comments") + } + Self::MalformedCommentString(x) => { + write!(f, "malformed comment string `{x}`") + } + Self::MalformedData => { + write!(f, "malformed comment data") + } + Self::MalformedPicture => { + write!(f, "malformed picture data") + } + } + } +} + +impl std::error::Error for VorbisCommentDecodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::IoError(x) => Some(x), + Self::FailedStringDecode(x) => Some(x), + _ => None, + } + } +} + +impl From for VorbisCommentDecodeError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +impl From for VorbisCommentDecodeError { + fn from(value: FromUtf8Error) -> Self { + Self::FailedStringDecode(value) + } +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum VorbisCommentEncodeError { + /// We encountered an IoError while processing a block + IoError(std::io::Error), + + /// We could not encode picture data + PictureEncodeError, +} + +impl Display for VorbisCommentEncodeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::IoError(_) => write!(f, "io error while reading vorbis comments"), + Self::PictureEncodeError => { + write!(f, "could not encode picture") + } + } + } +} + +impl std::error::Error for VorbisCommentEncodeError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::IoError(x) => Some(x), + _ => None, + } + } +} + +impl From for VorbisCommentEncodeError { + fn from(value: std::io::Error) -> Self { + Self::IoError(value) + } +} + +/// A decoded vorbis comment block +#[derive(Debug)] +pub struct VorbisComment { + /// This comment's vendor string + pub vendor: SmartString, + + /// List of (tag, value) + /// Repeated tags are allowed! + pub comments: Vec<(TagType, SmartString)>, + + /// A list of pictures found in this comment + pub pictures: Vec, +} + +impl VorbisComment { + /// Try to decode the given data as a vorbis comment block + pub fn decode(data: &[u8]) -> Result { + let mut d = Cursor::new(data); + + // This is re-used whenever we need to read four bytes + let mut block = [0u8; 4]; + + let vendor = { + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| VorbisCommentDecodeError::MalformedData)?; + + let length = u32::from_le_bytes(block); + let mut text = vec![0u8; length.try_into().unwrap()]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut text) + .map_err(|_| VorbisCommentDecodeError::MalformedData)?; + + String::from_utf8(text)? + }; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| VorbisCommentDecodeError::MalformedData)?; + let n_comments: usize = u32::from_le_bytes(block).try_into().unwrap(); + + let mut comments = Vec::new(); + let mut pictures = Vec::new(); + for _ in 0..n_comments { + let comment = { + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| VorbisCommentDecodeError::MalformedData)?; + + let length = u32::from_le_bytes(block); + let mut text = vec![0u8; length.try_into().unwrap()]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut text) + .map_err(|_| VorbisCommentDecodeError::MalformedData)?; + + String::from_utf8(text)? + }; + let (var, val) = + comment + .split_once('=') + .ok_or(VorbisCommentDecodeError::MalformedCommentString( + comment.clone(), + ))?; + + if !val.is_empty() { + if var.to_uppercase() == "METADATA_BLOCK_PICTURE" { + #[expect(clippy::map_err_ignore)] + pictures.push( + FlacPictureBlock::decode( + &base64::prelude::BASE64_STANDARD + .decode(val) + .map_err(|_| VorbisCommentDecodeError::MalformedPicture)?, + ) + .map_err(|_| VorbisCommentDecodeError::MalformedPicture)?, + ); + } else { + // Make sure empty strings are saved as "None" + comments.push(( + match &var.to_uppercase()[..] { + "TITLE" => TagType::TrackTitle, + "ALBUM" => TagType::Album, + "TRACKNUMBER" => TagType::TrackNumber, + "ARTIST" => TagType::TrackArtist, + "ALBUMARTIST" => TagType::AlbumArtist, + "GENRE" => TagType::Genre, + "ISRC" => TagType::Isrc, + "DATE" => TagType::ReleaseDate, + "TOTALTRACKS" => TagType::TrackTotal, + "LYRICS" => TagType::Lyrics, + x => TagType::Other(x.into()), + }, + val.into(), + )); + } + }; + } + + Ok(Self { + vendor: vendor.into(), + comments, + pictures, + }) + } +} + +impl VorbisComment { + /// Get the number of bytes that `encode()` will write. + pub fn get_len(&self) -> u32 { + let mut sum: u32 = 0; + sum += u32::try_from(self.vendor.len()).unwrap() + 4; + sum += 4; + + for (tagtype, value) in &self.comments { + let tagtype_str = match tagtype { + TagType::TrackTitle => "TITLE", + TagType::Album => "ALBUM", + TagType::TrackNumber => "TRACKNUMBER", + TagType::TrackArtist => "ARTIST", + TagType::AlbumArtist => "ALBUMARTIST", + TagType::Genre => "GENRE", + TagType::Isrc => "ISRC", + TagType::ReleaseDate => "DATE", + TagType::TrackTotal => "TOTALTRACKS", + TagType::Lyrics => "LYRICS", + TagType::Comment => "COMMENT", + TagType::DiskNumber => "DISKNUMBER", + TagType::DiskTotal => "DISKTOTAL", + TagType::Year => "YEAR", + TagType::Other(x) => x, + } + .to_uppercase(); + + let str = format!("{tagtype_str}={value}"); + sum += 4 + u32::try_from(str.len()).unwrap(); + } + + for p in &self.pictures { + // Compute b64 len + let mut x = p.get_len(); + if x % 3 != 0 { + x -= x % 3; + x += 3; + } + + #[expect(clippy::integer_division)] + { + sum += 4 * (x / 3); + } + + // Add "METADATA_BLOCK_PICTURE=" + sum += 23; + + // Add length bytes + sum += 4; + } + + return sum; + } + + /// Try to encode this vorbis comment + pub fn encode(&self, target: &mut impl Write) -> Result<(), VorbisCommentEncodeError> { + target.write_all(&u32::try_from(self.vendor.len()).unwrap().to_le_bytes())?; + target.write_all(self.vendor.as_bytes())?; + + target.write_all( + &u32::try_from(self.comments.len() + self.pictures.len()) + .unwrap() + .to_le_bytes(), + )?; + + for (tagtype, value) in &self.comments { + let tagtype_str = match tagtype { + TagType::TrackTitle => "TITLE", + TagType::Album => "ALBUM", + TagType::TrackNumber => "TRACKNUMBER", + TagType::TrackArtist => "ARTIST", + TagType::AlbumArtist => "ALBUMARTIST", + TagType::Genre => "GENRE", + TagType::Isrc => "ISRC", + TagType::ReleaseDate => "DATE", + TagType::TrackTotal => "TOTALTRACKS", + TagType::Lyrics => "LYRICS", + TagType::Comment => "COMMENT", + TagType::DiskNumber => "DISKNUMBER", + TagType::DiskTotal => "DISKTOTAL", + TagType::Year => "YEAR", + TagType::Other(x) => x, + } + .to_uppercase(); + + let str = format!("{tagtype_str}={value}"); + target.write_all(&u32::try_from(str.len()).unwrap().to_le_bytes())?; + target.write_all(str.as_bytes())?; + } + + for p in &self.pictures { + let mut pic_data = Vec::new(); + + #[expect(clippy::map_err_ignore)] + p.encode(false, false, &mut pic_data) + .map_err(|_| VorbisCommentEncodeError::PictureEncodeError)?; + + let pic_string = format!( + "METADATA_BLOCK_PICTURE={}", + &base64::prelude::BASE64_STANDARD.encode(&pic_data) + ); + + target.write_all(&u32::try_from(pic_string.len()).unwrap().to_le_bytes())?; + target.write_all(pic_string.as_bytes())?; + } + + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blockread.rs b/crates/pile-audio/src/flac/blockread.rs new file mode 100644 index 0000000..2c7394a --- /dev/null +++ b/crates/pile-audio/src/flac/blockread.rs @@ -0,0 +1,840 @@ +//! Strip metadata from a FLAC file without loading the whole thing into memory. + +use std::{ + collections::VecDeque, + io::{Cursor, Read, Seek, Write}, +}; +use thiserror::Error; + +use super::{ + blocks::{ + FlacAudioFrame, FlacCommentBlock, FlacMetablockDecode, FlacMetablockEncode, + FlacMetablockHeader, FlacMetablockType, + }, + errors::{FlacDecodeError, FlacEncodeError}, +}; +use crate::flac::blocks::{ + FlacApplicationBlock, FlacCuesheetBlock, FlacPaddingBlock, FlacPictureBlock, + FlacSeektableBlock, FlacStreaminfoBlock, +}; + +const MIN_AUDIO_FRAME_LEN: usize = 5000; + +/// Select which blocks we want to keep. +/// All values are `false` by default. +#[derive(Debug, Default, Clone, Copy)] +pub struct FlacBlockSelector { + /// Select `FlacMetablockType::Streaminfo` blocks. + pub pick_streaminfo: bool, + + /// Select `FlacMetablockType::Padding` blocks. + pub pick_padding: bool, + + /// Select `FlacMetablockType::Application` blocks. + pub pick_application: bool, + + /// Select `FlacMetablockType::SeekTable` blocks. + pub pick_seektable: bool, + + /// Select `FlacMetablockType::VorbisComment` blocks. + pub pick_vorbiscomment: bool, + + /// Select `FlacMetablockType::CueSheet` blocks. + pub pick_cuesheet: bool, + + /// Select `FlacMetablockType::Picture` blocks. + pub pick_picture: bool, + + /// Select audio frames. + pub pick_audio: bool, +} + +impl FlacBlockSelector { + /// Make a new [`FlacBlockSelector`] + pub fn new() -> Self { + Self::default() + } + + fn should_pick_meta(&self, block_type: FlacMetablockType) -> bool { + match block_type { + FlacMetablockType::Streaminfo => self.pick_streaminfo, + FlacMetablockType::Padding => self.pick_padding, + FlacMetablockType::Application => self.pick_application, + FlacMetablockType::Seektable => self.pick_seektable, + FlacMetablockType::VorbisComment => self.pick_vorbiscomment, + FlacMetablockType::Cuesheet => self.pick_cuesheet, + FlacMetablockType::Picture => self.pick_picture, + } + } +} + +enum FlacBlockType { + MagicBits { + data: [u8; 4], + left_to_read: usize, + }, + MetablockHeader { + is_first: bool, + data: [u8; 4], + left_to_read: usize, + }, + MetaBlock { + header: FlacMetablockHeader, + data: Vec, + }, + AudioData { + data: Vec, + }, +} + +#[derive(Debug)] +#[allow(missing_docs)] +pub enum FlacBlock { + Streaminfo(FlacStreaminfoBlock), + Picture(FlacPictureBlock), + Padding(FlacPaddingBlock), + Application(FlacApplicationBlock), + SeekTable(FlacSeektableBlock), + VorbisComment(FlacCommentBlock), + CueSheet(FlacCuesheetBlock), + AudioFrame(FlacAudioFrame), +} + +impl FlacBlock { + /// Encode this block + pub fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl Write, + ) -> Result<(), FlacEncodeError> { + match self { + Self::Streaminfo(b) => b.encode(is_last, with_header, target), + Self::SeekTable(b) => b.encode(is_last, with_header, target), + Self::Picture(b) => b.encode(is_last, with_header, target), + Self::Padding(b) => b.encode(is_last, with_header, target), + Self::Application(b) => b.encode(is_last, with_header, target), + Self::VorbisComment(b) => b.encode(is_last, with_header, target), + Self::CueSheet(b) => b.encode(is_last, with_header, target), + Self::AudioFrame(b) => b.encode(target), + } + } + + /// Try to decode the given data as a block + pub fn decode(block_type: FlacMetablockType, data: &[u8]) -> Result { + Ok(match block_type { + FlacMetablockType::Streaminfo => { + FlacBlock::Streaminfo(FlacStreaminfoBlock::decode(data)?) + } + FlacMetablockType::Application => { + FlacBlock::Application(FlacApplicationBlock::decode(data)?) + } + FlacMetablockType::Cuesheet => FlacBlock::CueSheet(FlacCuesheetBlock::decode(data)?), + FlacMetablockType::Padding => FlacBlock::Padding(FlacPaddingBlock::decode(data)?), + FlacMetablockType::Picture => FlacBlock::Picture(FlacPictureBlock::decode(data)?), + FlacMetablockType::Seektable => FlacBlock::SeekTable(FlacSeektableBlock::decode(data)?), + FlacMetablockType::VorbisComment => { + FlacBlock::VorbisComment(FlacCommentBlock::decode(data)?) + } + }) + } +} + +/// An error produced by a [`FlacBlockReader`] +#[derive(Debug, Error)] +pub enum FlacBlockReaderError { + /// Could not decode flac data + #[error("decode error while reading flac blocks")] + DecodeError(#[from] FlacDecodeError), + + /// Tried to finish or push data to a finished reader. + #[error("flac block reader is already finished")] + AlreadyFinished, +} + +/// A buffered flac block reader. +/// Use `push_data` to add flac data into this struct, +/// use `pop_block` to read flac blocks. +/// +/// This is the foundation of all other flac processors +/// we offer in this crate. +pub struct FlacBlockReader { + // Which blocks should we return? + selector: FlacBlockSelector, + + // The block we're currently reading. + // If this is `None`, we've called `finish()`. + current_block: Option, + + // Blocks we pick go here + output_blocks: VecDeque, +} + +impl FlacBlockReader { + /// Pop the next block we've read, if any. + pub fn pop_block(&mut self) -> Option { + self.output_blocks.pop_front() + } + + /// If true, this reader has received all the data it needs. + pub fn is_done(&self) -> bool { + self.current_block.is_none() + } + + /// If true, this reader has at least one block ready to pop. + /// Calling `pop_block` will return `Some(_)` if this is true. + pub fn has_block(&self) -> bool { + !self.output_blocks.is_empty() + } + + /// Make a new [`FlacBlockReader`]. + pub fn new(selector: FlacBlockSelector) -> Self { + Self { + selector, + current_block: Some(FlacBlockType::MagicBits { + data: [0; 4], + left_to_read: 4, + }), + + output_blocks: VecDeque::new(), + } + } + + /// Pass the given data through this block extractor. + /// Output data is stored in an internal buffer, and should be accessed + /// through `Read`. + pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { + let mut buf = Cursor::new(buf); + let mut last_read_size = 1; + + if self.current_block.is_none() { + return Err(FlacBlockReaderError::AlreadyFinished); + } + + 'outer: while last_read_size != 0 { + match self.current_block.as_mut().unwrap() { + FlacBlockType::MagicBits { data, left_to_read } => { + last_read_size = buf.read(&mut data[4 - *left_to_read..4]).unwrap(); + *left_to_read -= last_read_size; + + if *left_to_read == 0 { + if *data != [0x66, 0x4C, 0x61, 0x43] { + return Err(FlacDecodeError::BadMagicBytes.into()); + }; + + self.current_block = Some(FlacBlockType::MetablockHeader { + is_first: true, + data: [0; 4], + left_to_read: 4, + }) + } + } + + FlacBlockType::MetablockHeader { + is_first, + data, + left_to_read, + } => { + last_read_size = buf.read(&mut data[4 - *left_to_read..4]).unwrap(); + *left_to_read -= last_read_size; + + if *left_to_read == 0 { + let header = FlacMetablockHeader::decode(data)?; + if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo) + { + return Err(FlacDecodeError::BadFirstBlock.into()); + } + + self.current_block = Some(FlacBlockType::MetaBlock { + header, + data: Vec::new(), + }) + } + } + + FlacBlockType::MetaBlock { header, data } => { + last_read_size = buf + .by_ref() + .take(u64::from(header.length) - u64::try_from(data.len()).unwrap()) + .read_to_end(data) + .unwrap(); + + if data.len() == usize::try_from(header.length).unwrap() { + // If we picked this block type, add it to the queue + if self.selector.should_pick_meta(header.block_type) { + let b = FlacBlock::decode(header.block_type, data)?; + self.output_blocks.push_back(b); + } + + // Start next block + if header.is_last { + self.current_block = Some(FlacBlockType::AudioData { data: Vec::new() }) + } else { + self.current_block = Some(FlacBlockType::MetablockHeader { + is_first: false, + data: [0; 4], + left_to_read: 4, + }) + } + } + } + + FlacBlockType::AudioData { data } => { + // Limit the number of bytes we read at once, so we don't re-clone + // large amounts of data if `buf` contains multiple sync sequences. + // 5kb is a pretty reasonable frame size. + last_read_size = buf.by_ref().take(5_000).read_to_end(data).unwrap(); + if last_read_size == 0 { + continue 'outer; + } + + // We can't run checks if we don't have enough data. + if data.len() <= 2 { + continue; + } + + // Check frame sync header + // (`if` makes sure we only do this once) + if data.len() - last_read_size <= 2 + && !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) + { + return Err(FlacDecodeError::BadSyncBytes.into()); + } + + if data.len() >= MIN_AUDIO_FRAME_LEN { + // Look for a frame sync header in the data we read + // + // This isn't the *correct* way to split audio frames (false sync bytes can occur in audio data), + // but it's good enough for now---we don't decode audio data anyway. + // + // We could split on every sequence of sync bytes, but that's not any less wrong than the approach here. + // Also, it's slower---we'd rather have few large frames than many small ones. + + let first_byte = if data.len() - last_read_size < MIN_AUDIO_FRAME_LEN { + MIN_AUDIO_FRAME_LEN + 1 + } else { + data.len() - last_read_size + MIN_AUDIO_FRAME_LEN + 1 + }; + + // `i` is the index of the first byte *after* the sync sequence. + // + // This may seem odd, but it makes the odd edge case easier to handle: + // If we instead have `i` be the index of the first byte *of* the frame sequence, + // dealing with the case where `data` contained half the sync sequence before + // reading is tricky. + for i in first_byte..data.len() { + if data[i - 2] == 0b1111_1111 + && data[i - 1] & 0b1111_1100 == 0b1111_1000 + { + // We found another frame sync header. Split at this index. + if self.selector.pick_audio { + self.output_blocks.push_back(FlacBlock::AudioFrame( + FlacAudioFrame::decode(&data[0..i - 2])?, + )); + } + + // Backtrack to the first bit AFTER this new sync sequence + buf.seek(std::io::SeekFrom::Current( + -i64::try_from(data.len() - i).unwrap(), + )) + .unwrap(); + + self.current_block = Some(FlacBlockType::AudioData { + data: { + let mut v = Vec::with_capacity(MIN_AUDIO_FRAME_LEN); + v.extend(&data[i - 2..i]); + v + }, + }); + continue 'outer; + } + } + } + } + } + } + + return Ok(()); + } + + /// Finish reading data. + /// This tells the reader that it has received the entire stream. + /// + /// `finish()` should be called exactly once once we have finished each stream. + /// Finishing twice or pushing data to a finished reader results in a panic. + pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { + match self.current_block.take() { + None => return Err(FlacBlockReaderError::AlreadyFinished), + + Some(FlacBlockType::AudioData { data }) => { + // We can't run checks if we don't have enough data. + if data.len() <= 2 { + return Err(FlacDecodeError::MalformedBlock.into()); + } + + if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) { + return Err(FlacDecodeError::BadSyncBytes.into()); + } + + if self.selector.pick_audio { + self.output_blocks + .push_back(FlacBlock::AudioFrame(FlacAudioFrame::decode(&data)?)); + } + + self.current_block = None; + return Ok(()); + } + + // All other blocks have a known length and + // are finished automatically. + _ => return Err(FlacDecodeError::MalformedBlock.into()), + } + } +} + +#[cfg(test)] +mod tests { + use itertools::Itertools; + use paste::paste; + use rand::Rng; + use sha2::{Digest, Sha256}; + use std::{io::Write, ops::Range, str::FromStr}; + + use super::*; + use crate::{ + common::tagtype::TagType, + flac::tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue, manifest}, + }; + + fn read_file( + test_case: &FlacTestCase, + fragment_size_range: Option>, + selector: FlacBlockSelector, + ) -> Result, FlacBlockReaderError> { + let file_data = std::fs::read(test_case.get_path()).unwrap(); + + // Make sure input file is correct + let mut hasher = Sha256::new(); + hasher.update(&file_data); + assert_eq!( + test_case.get_in_hash(), + hasher.finalize().map(|x| format!("{x:02x}")).join("") + ); + + let mut reader = FlacBlockReader::new(selector); + let mut out_blocks = Vec::new(); + + // Push file data to the reader, in parts or as a whole. + if let Some(fragment_size_range) = fragment_size_range { + let mut head = 0; + while head < file_data.len() { + let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); + if head + frag_size > file_data.len() { + frag_size = file_data.len() - head; + } + reader.push_data(&file_data[head..head + frag_size])?; + head += frag_size; + } + } else { + reader.push_data(&file_data)?; + } + + reader.finish()?; + while let Some(b) = reader.pop_block() { + out_blocks.push(b) + } + + return Ok(out_blocks); + } + + fn test_identical( + test_case: &FlacTestCase, + fragment_size_range: Option>, + ) -> Result<(), FlacBlockReaderError> { + let out_blocks = read_file( + test_case, + fragment_size_range, + FlacBlockSelector { + pick_streaminfo: true, + pick_padding: true, + pick_application: true, + pick_seektable: true, + pick_vorbiscomment: true, + pick_cuesheet: true, + pick_picture: true, + pick_audio: true, + }, + )?; + + let mut out = Vec::new(); + out.write_all(&[0x66, 0x4C, 0x61, 0x43]).unwrap(); + + for i in 0..out_blocks.len() { + let b = &out_blocks[i]; + let is_last = if i == out_blocks.len() - 1 { + false + } else { + !matches!(b, FlacBlock::AudioFrame(_)) + && matches!(&out_blocks[i + 1], FlacBlock::AudioFrame(_)) + }; + + b.encode(is_last, true, &mut out).unwrap(); + } + + let mut hasher = Sha256::new(); + hasher.update(&out); + let result = hasher.finalize().map(|x| format!("{x:02x}")).join(""); + assert_eq!(result, test_case.get_in_hash(), "Output hash doesn't match"); + return Ok(()); + } + + fn test_blockread( + test_case: &FlacTestCase, + fragment_size_range: Option>, + ) -> Result<(), FlacBlockReaderError> { + let out_blocks = read_file( + test_case, + fragment_size_range, + FlacBlockSelector { + pick_streaminfo: true, + pick_padding: true, + pick_application: true, + pick_seektable: true, + pick_vorbiscomment: true, + pick_cuesheet: true, + pick_picture: true, + pick_audio: true, + }, + )?; + + assert_eq!( + test_case.get_blocks().unwrap().len(), + out_blocks + .iter() + .filter(|x| !matches!(*x, FlacBlock::AudioFrame(_))) + .count(), + "Number of blocks didn't match" + ); + + let mut audio_data_hasher = Sha256::new(); + let mut result_i = 0; + + for b in out_blocks { + match b { + FlacBlock::Streaminfo(s) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Streaminfo { + min_block_size, + max_block_size, + min_frame_size, + max_frame_size, + sample_rate, + channels, + bits_per_sample, + total_samples, + md5_signature, + } => { + assert_eq!(*min_block_size, s.min_block_size,); + assert_eq!(*max_block_size, s.max_block_size); + assert_eq!(*min_frame_size, s.min_frame_size); + assert_eq!(*max_frame_size, s.max_frame_size); + assert_eq!(*sample_rate, s.sample_rate); + assert_eq!(*channels, s.channels); + assert_eq!(*bits_per_sample, s.bits_per_sample); + assert_eq!(*total_samples, s.total_samples); + assert_eq!( + *md5_signature, + s.md5_signature.iter().map(|x| format!("{x:02x}")).join("") + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Application(a) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Application { + application_id, + hash, + } => { + assert_eq!( + *application_id, a.application_id, + "Application id doesn't match" + ); + assert_eq!( + *hash, + { + let mut hasher = Sha256::new(); + + hasher.update(&a.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }, + "Application content hash doesn't match" + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::CueSheet(c) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::CueSheet { hash } => { + assert_eq!(*hash, { + let mut hasher = Sha256::new(); + + hasher.update(&c.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Padding(p) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Padding { size } => { + assert_eq!(p.size, *size); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::SeekTable(t) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Seektable { hash } => { + assert_eq!(*hash, { + let mut hasher = Sha256::new(); + + hasher.update(&t.data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::Picture(p) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::Picture { + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + } => { + assert_eq!(*picture_type, p.picture_type, "{}", test_case.get_name()); + assert_eq!(*mime, p.mime, "{}", test_case.get_name()); + assert_eq!(*description, p.description, "{}", test_case.get_name()); + assert_eq!(*width, p.width, "{}", test_case.get_name()); + assert_eq!(*height, p.height, "{}", test_case.get_name()); + assert_eq!(*bit_depth, p.bit_depth, "{}", test_case.get_name()); + assert_eq!(*color_count, p.color_count, "{}", test_case.get_name()); + assert_eq!( + *img_data, + { + let mut hasher = Sha256::new(); + + hasher.update(&p.img_data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }, + "{}", + test_case.get_name() + ); + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::VorbisComment(v) => match &test_case.get_blocks().unwrap()[result_i] { + FlacBlockOutput::VorbisComment { + vendor, + comments, + pictures, + } => { + assert_eq!(*vendor, v.comment.vendor, "Comment vendor doesn't match"); + + assert_eq!( + v.comment.pictures.len(), + pictures.len(), + "Number of pictures doesn't match" + ); + + for (p, e) in v.comment.pictures.iter().zip(*pictures) { + match e { + FlacBlockOutput::Picture { + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + } => { + assert_eq!(*picture_type, p.picture_type); + assert_eq!(*mime, p.mime); + assert_eq!(*description, p.description); + assert_eq!(*width, p.width); + assert_eq!(*height, p.height); + assert_eq!(*bit_depth, p.bit_depth); + assert_eq!(*color_count, p.color_count); + assert_eq!(*img_data, { + let mut hasher = Sha256::new(); + hasher.update(&p.img_data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }); + } + _ => panic!("Bad test data: expected only Picture blocks."), + } + } + + match comments { + VorbisCommentTestValue::Raw { tags } => { + assert_eq!( + v.comment.comments.len(), + tags.len(), + "Number of comments doesn't match" + ); + + for ((got_tag, got_val), (exp_tag, exp_val)) in + v.comment.comments.iter().zip(*tags) + { + assert_eq!( + *got_tag, + TagType::from_str(exp_tag).unwrap(), + "Tag key doesn't match" + ); + assert_eq!( + got_val, exp_val, + "Tag value of {exp_tag} doesn't match" + ); + } + } + + VorbisCommentTestValue::Hash { n_comments, hash } => { + assert_eq!( + v.comment.comments.len(), + *n_comments, + "Number of comments doesn't match" + ); + + let mut hasher = Sha256::new(); + + for (got_tag, got_val) in v.comment.comments { + hasher.update(format!("{got_tag}={got_val};").as_bytes()); + } + assert_eq!( + &hasher.finalize().map(|x| format!("{x:02x}")).join(""), + hash, + "Comment hash doesn't match" + ); + } + } + } + _ => panic!("Unexpected block type"), + }, + + FlacBlock::AudioFrame(data) => { + let mut vec = Vec::new(); + data.encode(&mut vec).unwrap(); + audio_data_hasher.update(&vec); + + if result_i != test_case.get_blocks().unwrap().len() { + panic!("There are metadata blocks between audio frames!") + } + + // Don't increment result_i + continue; + } + } + + result_i += 1; + } + + // Check audio data hash + assert_eq!( + test_case.get_audio_hash().unwrap(), + audio_data_hasher + .finalize() + .map(|x| format!("{x:02x}")) + .join("") + ); + + return Ok(()); + } + + // Helper macros to generate tests + macro_rules! gen_tests { + ( $test_name:ident ) => { + paste! { + #[test] + pub fn []() { + let manifest = manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + + match test_case { + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_blockread( + test_case, + Some(1..256), + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_blockread(test_case, Some(1..256)).unwrap_err(); + match e { + FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + + #[test] + pub fn []() { + let manifest = manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + + match test_case { + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_identical( + test_case, + Some(1..256), + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_identical(test_case, Some(1..256)).unwrap_err(); + match e { + FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + } + }; + } + + gen_tests!(custom_01); + gen_tests!(custom_02); + gen_tests!(custom_03); + + gen_tests!(uncommon_10); + + gen_tests!(faulty_06); + gen_tests!(faulty_07); + gen_tests!(faulty_10); + gen_tests!(faulty_11); + + gen_tests!(subset_45); + gen_tests!(subset_46); + gen_tests!(subset_47); + gen_tests!(subset_48); + gen_tests!(subset_49); + gen_tests!(subset_50); + gen_tests!(subset_51); + gen_tests!(subset_52); + gen_tests!(subset_53); + gen_tests!(subset_54); + gen_tests!(subset_55); + gen_tests!(subset_56); + gen_tests!(subset_57); + gen_tests!(subset_58); + gen_tests!(subset_59); +} diff --git a/crates/pile-audio/src/flac/blocks/application.rs b/crates/pile-audio/src/flac/blocks/application.rs new file mode 100644 index 0000000..cd2f378 --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/application.rs @@ -0,0 +1,76 @@ +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; +use std::{ + fmt::Debug, + io::{Cursor, Read}, +}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// An application block in a flac file +pub struct FlacApplicationBlock { + /// Registered application ID + pub application_id: u32, + + /// The application data + pub data: Vec, +} + +impl Debug for FlacApplicationBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacApplicationBlock") + .field("application_id", &self.application_id) + .field("data_len", &self.data.len()) + .finish() + } +} + +impl FlacMetablockDecode for FlacApplicationBlock { + fn decode(data: &[u8]) -> Result { + let mut d = Cursor::new(data); + + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + let application_id = u32::from_be_bytes(block); + + let data = { + let mut data = Vec::with_capacity(data.len()); + d.read_to_end(&mut data)?; + data + }; + + Ok(Self { + application_id, + data, + }) + } +} + +impl FlacMetablockEncode for FlacApplicationBlock { + fn get_len(&self) -> u32 { + (self.data.len() + 4).try_into().unwrap() + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Application, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + target.write_all(&self.application_id.to_be_bytes())?; + target.write_all(&self.data)?; + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/audiodata.rs b/crates/pile-audio/src/flac/blocks/audiodata.rs new file mode 100644 index 0000000..6d29eae --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/audiodata.rs @@ -0,0 +1,43 @@ +use std::fmt::Debug; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +/// An audio frame in a flac file +pub struct FlacAudioFrame { + /// The audio frame + pub data: Vec, +} + +impl Debug for FlacAudioFrame { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacAudioFrame") + .field("data_len", &self.data.len()) + .finish() + } +} + +impl FlacAudioFrame { + /// Decode the given data as a flac audio frame. + /// This should start with a sync sequence. + pub fn decode(data: &[u8]) -> Result { + if data.len() <= 2 { + return Err(FlacDecodeError::MalformedBlock); + } + + if !(data[0] == 0b1111_1111 && data[1] & 0b1111_1100 == 0b1111_1000) { + return Err(FlacDecodeError::BadSyncBytes); + } + + Ok(Self { + data: Vec::from(data), + }) + } +} + +impl FlacAudioFrame { + /// Encode this audio frame. + pub fn encode(&self, target: &mut impl std::io::Write) -> Result<(), FlacEncodeError> { + target.write_all(&self.data)?; + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/comment.rs b/crates/pile-audio/src/flac/blocks/comment.rs new file mode 100644 index 0000000..91f9fad --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/comment.rs @@ -0,0 +1,54 @@ +use std::fmt::Debug; + +use crate::{ + common::vorbiscomment::VorbisComment, + flac::errors::{FlacDecodeError, FlacEncodeError}, +}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A vorbis comment metablock in a flac file +pub struct FlacCommentBlock { + /// The vorbis comment stored inside this block + pub comment: VorbisComment, +} + +impl Debug for FlacCommentBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacCommentBlock") + .field("comment", &self.comment) + .finish() + } +} + +impl FlacMetablockDecode for FlacCommentBlock { + fn decode(data: &[u8]) -> Result { + let comment = VorbisComment::decode(data)?; + Ok(Self { comment }) + } +} + +impl FlacMetablockEncode for FlacCommentBlock { + fn get_len(&self) -> u32 { + self.comment.get_len() + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::VorbisComment, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + self.comment.encode(target)?; + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/cuesheet.rs b/crates/pile-audio/src/flac/blocks/cuesheet.rs new file mode 100644 index 0000000..5a013ac --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/cuesheet.rs @@ -0,0 +1,50 @@ +use std::fmt::Debug; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A cuesheet meta in a flac file +pub struct FlacCuesheetBlock { + /// The seek table + pub data: Vec, +} + +impl Debug for FlacCuesheetBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacAudioFrame") + .field("data_len", &self.data.len()) + .finish() + } +} + +impl FlacMetablockDecode for FlacCuesheetBlock { + fn decode(data: &[u8]) -> Result { + Ok(Self { data: data.into() }) + } +} + +impl FlacMetablockEncode for FlacCuesheetBlock { + fn get_len(&self) -> u32 { + self.data.len().try_into().unwrap() + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Cuesheet, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + target.write_all(&self.data)?; + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/header.rs b/crates/pile-audio/src/flac/blocks/header.rs new file mode 100644 index 0000000..f60def6 --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/header.rs @@ -0,0 +1,87 @@ +//! FLAC metablock headers. See spec. +use std::fmt::Debug; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +/// A type of flac metadata block +#[allow(missing_docs)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum FlacMetablockType { + Streaminfo, + Padding, + Application, + Seektable, + VorbisComment, + Cuesheet, + Picture, +} + +impl FlacMetablockType { + /// Read and parse a metablock header from the given reader. + /// Returns (block_type, block_data_length, is_last) + pub(crate) fn from_id(id: u8) -> Result { + return Ok(match id & 0b01111111 { + 0 => FlacMetablockType::Streaminfo, + 1 => FlacMetablockType::Padding, + 2 => FlacMetablockType::Application, + 3 => FlacMetablockType::Seektable, + 4 => FlacMetablockType::VorbisComment, + 5 => FlacMetablockType::Cuesheet, + 6 => FlacMetablockType::Picture, + x => return Err(FlacDecodeError::BadMetablockType(x)), + }); + } +} + +/// The header of a flac metadata block +#[derive(Debug, Clone)] +pub struct FlacMetablockHeader { + /// The type of block this is + pub block_type: FlacMetablockType, + + /// The length of this block, in bytes + /// (not including this header) + pub length: u32, + + /// If true, this is the last metadata block + pub is_last: bool, +} + +impl FlacMetablockHeader { + /// Try to decode the given bytes as a flac metablock header + pub fn decode(header: &[u8]) -> Result { + if header.len() != 4 { + return Err(FlacDecodeError::MalformedBlock); + } + + return Ok(Self { + block_type: FlacMetablockType::from_id(header[0])?, + length: u32::from_be_bytes([0, header[1], header[2], header[3]]), + is_last: header[0] & 0b10000000 == 0b10000000, + }); + } +} + +impl FlacMetablockHeader { + /// Try to encode this header + pub fn encode(&self, target: &mut impl std::io::Write) -> Result<(), FlacEncodeError> { + let mut block_type = match self.block_type { + FlacMetablockType::Streaminfo => 0, + FlacMetablockType::Padding => 1, + FlacMetablockType::Application => 2, + FlacMetablockType::Seektable => 3, + FlacMetablockType::VorbisComment => 4, + FlacMetablockType::Cuesheet => 5, + FlacMetablockType::Picture => 6, + }; + + if self.is_last { + block_type |= 0b1000_0000; + }; + + let x = self.length.to_be_bytes(); + target.write_all(&[block_type, x[1], x[2], x[3]])?; + + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/mod.rs b/crates/pile-audio/src/flac/blocks/mod.rs new file mode 100644 index 0000000..e70c479 --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/mod.rs @@ -0,0 +1,58 @@ +//! Read and write impelementations for all flac block types + +// Not metadata blocks +mod header; +pub use header::{FlacMetablockHeader, FlacMetablockType}; + +mod audiodata; +pub use audiodata::FlacAudioFrame; + +// Metadata blocks + +mod streaminfo; +pub use streaminfo::FlacStreaminfoBlock; + +mod picture; +pub use picture::FlacPictureBlock; + +mod padding; +pub use padding::FlacPaddingBlock; + +mod application; +pub use application::FlacApplicationBlock; + +mod seektable; +pub use seektable::FlacSeektableBlock; + +mod cuesheet; +pub use cuesheet::FlacCuesheetBlock; + +mod comment; +pub use comment::FlacCommentBlock; + +use super::errors::{FlacDecodeError, FlacEncodeError}; +use std::io::Write; + +/// A decode implementation for a +/// flac metadata block +pub trait FlacMetablockDecode: Sized { + /// Try to decode this block from bytes. + /// `data` should NOT include the metablock header. + fn decode(data: &[u8]) -> Result; +} + +/// A encode implementation for a +/// flac metadata block +pub trait FlacMetablockEncode: Sized { + /// Get the number of bytes that `encode()` will write. + /// This does NOT include header length. + fn get_len(&self) -> u32; + + /// Try to encode this block as bytes. + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl Write, + ) -> Result<(), FlacEncodeError>; +} diff --git a/crates/pile-audio/src/flac/blocks/padding.rs b/crates/pile-audio/src/flac/blocks/padding.rs new file mode 100644 index 0000000..4ed2012 --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/padding.rs @@ -0,0 +1,50 @@ +use std::{fmt::Debug, io::Read}; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A padding block in a FLAC file. +#[derive(Debug)] +pub struct FlacPaddingBlock { + /// The length of this padding, in bytes. + pub size: u32, +} + +impl FlacMetablockDecode for FlacPaddingBlock { + fn decode(data: &[u8]) -> Result { + if data.iter().any(|x| *x != 0u8) { + return Err(FlacDecodeError::MalformedBlock); + } + + Ok(Self { + size: data.len().try_into().unwrap(), + }) + } +} + +impl FlacMetablockEncode for FlacPaddingBlock { + fn get_len(&self) -> u32 { + self.size + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Padding, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + std::io::copy(&mut std::io::repeat(0u8).take(self.size.into()), target)?; + + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/picture.rs b/crates/pile-audio/src/flac/blocks/picture.rs new file mode 100644 index 0000000..d590ba6 --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/picture.rs @@ -0,0 +1,197 @@ +use std::{ + fmt::Debug, + io::{Cursor, Read}, +}; + +use mime::Mime; + +use crate::{ + common::picturetype::PictureType, + flac::errors::{FlacDecodeError, FlacEncodeError}, +}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A picture metablock in a flac file +pub struct FlacPictureBlock { + /// The type of this picture + pub picture_type: PictureType, + + /// The format of this picture + pub mime: Mime, + + /// The description of this picture + pub description: String, + + /// The width of this picture, in px + pub width: u32, + + /// The height of this picture, in px + pub height: u32, + + /// The bit depth of this picture + pub bit_depth: u32, + + /// The color count of this picture (if indexed) + pub color_count: u32, + + /// The image data + pub img_data: Vec, +} + +impl Debug for FlacPictureBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacPicture") + .field("type", &self.picture_type) + .field("mime", &self.mime) + .field("img_data.len()", &self.img_data.len()) + .finish() + } +} + +impl FlacMetablockDecode for FlacPictureBlock { + fn decode(data: &[u8]) -> Result { + let mut d = Cursor::new(data); + + // This is re-used whenever we need to read four bytes + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + let picture_type = PictureType::from_idx(u32::from_be_bytes(block))?; + + // Image format + let mime = { + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + let mime_length = u32::from_be_bytes(block).try_into().unwrap(); + let mut mime = vec![0u8; mime_length]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut mime) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + String::from_utf8(mime) + .ok() + .and_then(|x| x.parse().ok()) + .unwrap_or(mime::APPLICATION_OCTET_STREAM) + }; + + // Image description + let description = { + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + let desc_length = u32::from_be_bytes(block).try_into().unwrap(); + let mut desc = vec![0u8; desc_length]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut desc) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + String::from_utf8(desc)? + }; + + // Image width + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + let width = u32::from_be_bytes(block); + + // Image height + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + let height = u32::from_be_bytes(block); + + // Image bit depth + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + let depth = u32::from_be_bytes(block); + + // Color count for indexed images + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + let color_count = u32::from_be_bytes(block); + + // Image data length + let img_data = { + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + let data_length = u32::from_be_bytes(block).try_into().unwrap(); + let mut img_data = vec![0u8; data_length]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut img_data) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + img_data + }; + + Ok(Self { + picture_type, + mime, + description, + width, + height, + bit_depth: depth, + color_count, + img_data, + }) + } +} + +impl FlacMetablockEncode for FlacPictureBlock { + fn get_len(&self) -> u32 { + (4 + (4 + self.mime.to_string().len()) + + (4 + self.description.len()) + + 4 + 4 + 4 + + 4 + (4 + self.img_data.len())) + .try_into() + .unwrap() + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Picture, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + target.write_all(&self.picture_type.to_idx().to_be_bytes())?; + + let mime = self.mime.to_string(); + target.write_all(&u32::try_from(mime.len()).unwrap().to_be_bytes())?; + target.write_all(self.mime.to_string().as_bytes())?; + drop(mime); + + target.write_all(&u32::try_from(self.description.len()).unwrap().to_be_bytes())?; + target.write_all(self.description.as_bytes())?; + + target.write_all(&self.width.to_be_bytes())?; + target.write_all(&self.height.to_be_bytes())?; + target.write_all(&self.bit_depth.to_be_bytes())?; + target.write_all(&self.color_count.to_be_bytes())?; + + target.write_all(&u32::try_from(self.img_data.len()).unwrap().to_be_bytes())?; + target.write_all(&self.img_data)?; + + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/seektable.rs b/crates/pile-audio/src/flac/blocks/seektable.rs new file mode 100644 index 0000000..6edf16e --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/seektable.rs @@ -0,0 +1,50 @@ +use std::fmt::Debug; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A seektable block in a flac file +pub struct FlacSeektableBlock { + /// The seek table + pub data: Vec, +} + +impl Debug for FlacSeektableBlock { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("FlacSeektableBlock") + .field("data_len", &self.data.len()) + .finish() + } +} + +impl FlacMetablockDecode for FlacSeektableBlock { + fn decode(data: &[u8]) -> Result { + Ok(Self { data: data.into() }) + } +} + +impl FlacMetablockEncode for FlacSeektableBlock { + fn get_len(&self) -> u32 { + self.data.len().try_into().unwrap() + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Seektable, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + target.write_all(&self.data)?; + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/blocks/streaminfo.rs b/crates/pile-audio/src/flac/blocks/streaminfo.rs new file mode 100644 index 0000000..ddaac9b --- /dev/null +++ b/crates/pile-audio/src/flac/blocks/streaminfo.rs @@ -0,0 +1,218 @@ +use std::io::{Cursor, Read}; + +use crate::flac::errors::{FlacDecodeError, FlacEncodeError}; + +use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType}; + +/// A streaminfo block in a flac file +#[derive(Debug)] +pub struct FlacStreaminfoBlock { + /// The minimum block size (in samples) used in the stream. + pub min_block_size: u32, + + /// The maximum block size (in samples) used in the stream. + /// (Minimum blocksize == maximum blocksize) implies a fixed-blocksize stream. + pub max_block_size: u32, + + /// The minimum frame size (in bytes) used in the stream. + /// May be 0 to imply the value is not known. + pub min_frame_size: u32, + + /// The minimum frame size (in bytes) used in the stream. + /// May be 0 to imply the value is not known. + pub max_frame_size: u32, + + /// Sample rate in Hz. Though 20 bits are available, + /// the maximum sample rate is limited by the structure of frame headers to 655350Hz. + /// Also, a value of 0 is invalid. + pub sample_rate: u32, + + /// (number of channels)-1. FLAC supports from 1 to 8 channels + pub channels: u8, + + /// (bits per sample)-1. FLAC supports from 4 to 32 bits per sample. + pub bits_per_sample: u8, + + /// Total samples in stream. 'Samples' means inter-channel sample, i.e. one second of 44.1Khz audio will have 44100 samples regardless of the number of channels. A value of zero here means the number of total samples is unknown. + pub total_samples: u128, + + /// MD5 signature of the unencoded audio data. This allows the decoder to determine if an error exists in the audio data even when the error does not result in an invalid bitstream. + pub md5_signature: [u8; 16], +} + +impl FlacMetablockDecode for FlacStreaminfoBlock { + fn decode(data: &[u8]) -> Result { + let mut d = Cursor::new(data); + + let min_block_size = { + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block[2..]) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + u32::from_be_bytes(block) + }; + + let max_block_size = { + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block[2..]) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + u32::from_be_bytes(block) + }; + + let min_frame_size = { + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block[1..]) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + u32::from_be_bytes(block) + }; + + let max_frame_size = { + let mut block = [0u8; 4]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block[1..]) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + u32::from_be_bytes(block) + }; + + let (sample_rate, channels, bits_per_sample, total_samples) = { + let mut block = [0u8; 8]; + + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + + ( + // 20 bits: sample rate in hz + u32::from_be_bytes([0, block[0], block[1], block[2]]) >> 4, + // 3 bits: number of channels - 1. + // FLAC supports 1 - 8 channels. + ((u8::from_le_bytes([block[2]]) & 0b0000_1110) >> 1) + 1, + // 5 bits: bits per sample - 1. + // FLAC supports 4 - 32 bps. + ((u8::from_le_bytes([block[2]]) & 0b0000_0001) << 4) + + ((u8::from_le_bytes([block[3]]) & 0b1111_0000) >> 4) + + 1, + // 36 bits: total "cross-channel" samples in the stream. + // (one second of 44.1Khz audio will have 44100 samples regardless of the number of channels) + // Zero means we don't know. + u128::from_be_bytes([ + 0, + 0, + 0, + 0, + // + 0, + 0, + 0, + 0, + // + 0, + 0, + 0, + block[3] & 0b0000_1111, + // + block[4], + block[5], + block[6], + block[7], + ]), + ) + }; + + let md5_signature = { + let mut block = [0u8; 16]; + #[expect(clippy::map_err_ignore)] + d.read_exact(&mut block) + .map_err(|_| FlacDecodeError::MalformedBlock)?; + block + }; + + Ok(Self { + min_block_size, + max_block_size, + min_frame_size, + max_frame_size, + sample_rate, + channels, + bits_per_sample, + total_samples, + md5_signature, + }) + } +} + +impl FlacMetablockEncode for FlacStreaminfoBlock { + fn get_len(&self) -> u32 { + 34 + } + + fn encode( + &self, + is_last: bool, + with_header: bool, + target: &mut impl std::io::Write, + ) -> Result<(), FlacEncodeError> { + if with_header { + let header = FlacMetablockHeader { + block_type: FlacMetablockType::Streaminfo, + length: self.get_len(), + is_last, + }; + header.encode(target)?; + } + + target.write_all(&self.min_block_size.to_be_bytes()[2..])?; + target.write_all(&self.max_block_size.to_be_bytes()[2..])?; + target.write_all(&self.min_frame_size.to_be_bytes()[1..])?; + target.write_all(&self.max_frame_size.to_be_bytes()[1..])?; + + // Layout of the next 8 bytes: + // [8]: full bytes + // [4 ]: first 4 bits are from this + // [ 3]: next 3 bits are from this + // + // [8][8][4 ]: Sample rate + // [ ][ ][ 3 ]: channels + // [ ][ ][ 1][4 ]: bits per sample + // [ ][ ][ ][ 4][8 x 4]: total samples + + let mut out = [0u8; 8]; + + let sample_rate = &self.sample_rate.to_be_bytes()[1..4]; + out[0] = (sample_rate[0] << 4) & 0b1111_0000; + out[0] |= (sample_rate[1] >> 4) & 0b0000_1111; + out[1] = (sample_rate[1] << 4) & 0b1111_0000; + out[1] |= (sample_rate[2] >> 4) & 0b000_1111; + out[2] = (sample_rate[2] << 4) & 0b1111_0000; + + let channels = self.channels - 1; + out[2] |= (channels << 1) & 0b0000_1110; + + let bits_per_sample = self.bits_per_sample - 1; + out[2] |= (bits_per_sample >> 4) & 0b0000_0001; + out[3] |= (bits_per_sample << 4) & 0b1111_0000; + + let total_samples = self.total_samples.to_be_bytes(); + out[3] |= total_samples[10] & 0b0000_1111; + out[4] = total_samples[12]; + out[5] = total_samples[13]; + out[6] = total_samples[14]; + out[7] = total_samples[15]; + + target.write_all(&out)?; + + target.write_all(&self.md5_signature)?; + + return Ok(()); + } +} diff --git a/crates/pile-audio/src/flac/errors.rs b/crates/pile-audio/src/flac/errors.rs new file mode 100644 index 0000000..36a2980 --- /dev/null +++ b/crates/pile-audio/src/flac/errors.rs @@ -0,0 +1,68 @@ +//! FLAC errors +use crate::common::{ + picturetype::PictureTypeError, + vorbiscomment::{VorbisCommentDecodeError, VorbisCommentEncodeError}, +}; +use std::string::FromUtf8Error; +use thiserror::Error; + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum FlacDecodeError { + /// FLAC does not start with 0x66 0x4C 0x61 0x43 + #[error("flac signature is missing or malformed")] + BadMagicBytes, + + /// The first metablock isn't StreamInfo + #[error("first metablock isn't streaminfo")] + BadFirstBlock, + + /// We got an invalid metadata block type + #[error("invalid flac metablock type {0}")] + BadMetablockType(u8), + + /// We encountered an i/o error while processing + #[error("io error while reading flac")] + IoError(#[from] std::io::Error), + + /// We could not parse a vorbis comment + #[error("error while decoding vorbis comment")] + VorbisComment(#[from] VorbisCommentDecodeError), + + /// We tried to decode a string, but found invalid UTF-8 + #[error("error while decoding string")] + FailedStringDecode(#[from] FromUtf8Error), + + /// We tried to read a block, but it was out of spec. + #[error("malformed flac block")] + MalformedBlock, + + /// We didn't find frame sync bytes where we expected them + #[error("bad frame sync bytes")] + BadSyncBytes, + + /// We tried to decode a bad picture type + #[error("bad picture type")] + PictureTypeError(#[from] PictureTypeError), +} + +#[allow(missing_docs)] +#[derive(Debug, Error)] +pub enum FlacEncodeError { + /// We encountered an i/o error while processing + #[error("io error while encoding block")] + IoError(#[from] std::io::Error), + + /// We could not encode a picture inside a vorbis comment + #[error("could not encode picture in vorbis comment")] + VorbisPictureEncodeError, +} + +impl From for FlacEncodeError { + fn from(value: VorbisCommentEncodeError) -> Self { + match value { + VorbisCommentEncodeError::IoError(e) => e.into(), + VorbisCommentEncodeError::PictureEncodeError => Self::VorbisPictureEncodeError, + } + } +} diff --git a/crates/pile-audio/src/flac/mod.rs b/crates/pile-audio/src/flac/mod.rs new file mode 100644 index 0000000..302a102 --- /dev/null +++ b/crates/pile-audio/src/flac/mod.rs @@ -0,0 +1,942 @@ +//! Parse FLAC metadata. + +pub mod blockread; +pub mod blocks; +pub mod errors; +pub mod proc; + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use itertools::Itertools; + use mime::Mime; + + use super::errors::FlacDecodeError; + use crate::common::{picturetype::PictureType, vorbiscomment::VorbisCommentDecodeError}; + + /// The value of a vorbis comment. + /// + /// Some files have VERY large comments, and providing them + /// explicitly here doesn't make sense. + #[derive(Clone)] + pub enum VorbisCommentTestValue { + /// The comments, in order + Raw { + tags: &'static [(&'static str, &'static str)], + }, + /// The hash of all comments concatenated together, + /// stringified as `{key}={value};` + Hash { + n_comments: usize, + hash: &'static str, + }, + } + + #[derive(Clone)] + pub enum FlacBlockOutput { + Application { + application_id: u32, + hash: &'static str, + }, + Streaminfo { + min_block_size: u32, + max_block_size: u32, + min_frame_size: u32, + max_frame_size: u32, + sample_rate: u32, + channels: u8, + bits_per_sample: u8, + total_samples: u128, + md5_signature: &'static str, + }, + CueSheet { + // Hash of this block's data, without the header. + // This is easy to get with + // + // ```notrust + // metaflac \ + // --list \ + // --block-number= \ + // --data-format=binary-headerless \ + // \ + // | sha256sum + //``` + hash: &'static str, + }, + Seektable { + hash: &'static str, + }, + Padding { + size: u32, + }, + Picture { + picture_type: PictureType, + mime: Mime, + description: &'static str, + width: u32, + height: u32, + bit_depth: u32, + color_count: u32, + img_data: &'static str, + }, + VorbisComment { + vendor: &'static str, + comments: VorbisCommentTestValue, + pictures: &'static [FlacBlockOutput], + }, + } + + pub enum FlacTestCase { + Success { + /// This test's name + test_name: &'static str, + + /// The file to use for this test + file_path: &'static str, + + /// The hash of the input files + in_hash: &'static str, + + /// The flac metablocks we expect to find in this file, in order. + blocks: Vec, + + /// The hash of the audio frames in this file + /// + /// Get this hash by running `metaflac --remove-all --dont-use-padding`, + /// then by manually deleting remaining headers in a hex editor + /// (Remember that the sync sequence is 0xFF 0xF8) + audio_hash: &'static str, + + /// The hash we should get when we strip this file's tags. + /// + /// A stripped flac file has unmodified STREAMINFO, SEEKTABLE, + /// CUESHEET, and audio data blocks; and nothing else (not even padding). + /// + /// Reference implementation: + /// ```notrust + /// metaflac \ + /// --remove \ + /// --block-type=PADDING,APPLICATION,VORBIS_COMMENT,PICTURE \ + /// --dont-use-padding \ + /// + /// ``` + stripped_hash: &'static str, + }, + Error { + /// This test's name + test_name: &'static str, + + /// The file to use for this test + file_path: &'static str, + + /// The hash of the input files + in_hash: &'static str, + + /// The error we should encounter while reading this file + check_error: &'static dyn Fn(&FlacDecodeError) -> bool, + + /// If some, stripping this file's metadata should produce the given hash. + /// If none, trying to strip metadata should produce `check_error` + stripped_hash: Option<&'static str>, + + /// If some, the following images should be extracted from this file + /// If none, trying to strip images should produce `check_error` + pictures: Option>, + }, + } + + impl FlacTestCase { + pub fn get_name(&self) -> &str { + match self { + Self::Error { test_name, .. } | Self::Success { test_name, .. } => test_name, + } + } + + pub fn get_path(&self) -> &str { + match self { + Self::Success { file_path, .. } | Self::Error { file_path, .. } => file_path, + } + } + + pub fn get_in_hash(&self) -> &str { + match self { + Self::Success { in_hash, .. } | Self::Error { in_hash, .. } => in_hash, + } + } + + pub fn get_stripped_hash(&self) -> Option<&str> { + match self { + Self::Success { stripped_hash, .. } => Some(stripped_hash), + Self::Error { stripped_hash, .. } => *stripped_hash, + } + } + + pub fn get_audio_hash(&self) -> Option<&str> { + match self { + Self::Success { audio_hash, .. } => Some(audio_hash), + _ => None, + } + } + + pub fn get_blocks(&self) -> Option<&[FlacBlockOutput]> { + match self { + Self::Success { blocks, .. } => Some(blocks), + _ => None, + } + } + + pub fn get_pictures(&self) -> Option> { + match self { + Self::Success { blocks, .. } => { + let mut out = Vec::new(); + for b in blocks { + match b { + FlacBlockOutput::Picture { .. } => out.push(b.clone()), + FlacBlockOutput::VorbisComment { pictures, .. } => { + for p in *pictures { + out.push(p.clone()) + } + } + _ => {} + } + } + + return Some(out); + } + + Self::Error { pictures, .. } => { + pictures.as_ref().map(|x| x.iter().cloned().collect()) + } + } + } + } + + /// A list of test files and their expected output + pub fn manifest() -> [FlacTestCase; 23] { + [ + FlacTestCase::Error { + test_name: "uncommon_10", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_uncommon/10 - file starting at frame header.flac" + ), + in_hash: "d95f63e8101320f5ac7ffe249bc429a209eb0e10996a987301eaa63386a8faa1", + check_error: &|x| matches!(x, FlacDecodeError::BadMagicBytes), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_06", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac" + ), + in_hash: "53aed5e7fde7a652b82ba06a8382b2612b02ebbde7b0d2016276644d17cc76cd", + check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_07", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac" + ), + in_hash: "6d46725991ba5da477187fde7709ea201c399d00027257c365d7301226d851ea", + check_error: &|x| matches!(x, FlacDecodeError::BadFirstBlock), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_10", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac" + ), + in_hash: "c79b0514a61634035a5653c5493797bbd1fcc78982116e4d429630e9e462d29b", + check_error: &|x| { + matches!( + x, + FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedData) + ) + }, + // This file's vorbis comment is invalid, but that shouldn't stop us from removing it. + // As a general rule, we should NOT encounter an error when stripping invalid blocks. + // + // We should, however, get errors when we try to strip flac files with invalid *structure* + // (For example, the out-of-order streaminfo test in faulty_07). + stripped_hash: Some( + "4b994f82dc1699a58e2b127058b37374220ee41dc294d4887ac14f056291a1b0", + ), + pictures: None, + }, + FlacTestCase::Error { + test_name: "faulty_11", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_faulty/11 - incorrect metadata block length.flac" + ), + in_hash: "3732151ba8c4e66a785165aa75a444aad814c16807ddc97b793811376acacfd6", + check_error: &|x| matches!(x, FlacDecodeError::BadMetablockType(127)), + stripped_hash: None, + pictures: None, + }, + FlacTestCase::Success { + test_name: "subset_45", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/45 - no total number of samples set.flac" + ), + in_hash: "336a18eb7a78f7fc0ab34980348e2895bc3f82db440a2430d9f92e996f889f9a", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 907, + max_frame_size: 8053, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 0, + md5_signature: "c41ae3b82c35d8f5c3dab1729f948fde", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "3fb3482ebc1724559bdd57f34de472458563d78a676029614e76e32b5d2b8816", + stripped_hash: "31631ac227ebe2689bac7caa1fa964b47e71a9f1c9c583a04ea8ebd9371508d0", + }, + FlacTestCase::Success { + test_name: "subset_46", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/46 - no min-max framesize set.flac" + ), + in_hash: "9dc39732ce17815832790901b768bb50cd5ff0cd21b28a123c1cabc16ed776cc", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 0, + max_frame_size: 0, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 282866, + md5_signature: "fd131e6ebc75251ed83f8f4c07df36a4", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "a1eed422462b386a932b9eb3dff3aea3687b41eca919624fb574aadb7eb50040", + stripped_hash: "9e57cd77f285fc31f87fa4e3a31ab8395d68d5482e174c8e0d0bba9a0c20ba27", + }, + FlacTestCase::Success { + test_name: "subset_47", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/47 - only STREAMINFO.flac" + ), + in_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", + blocks: vec![FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 4747, + max_frame_size: 7034, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 232608, + md5_signature: "bba30c5f70789910e404b7ac727c3853", + }], + audio_hash: "5ee1450058254087f58c91baf0f70d14bde8782cf2dc23c741272177fe0fce6e", + stripped_hash: "9a62c79f634849e74cb2183f9e3a9bd284f51e2591c553008d3e6449967eef85", + }, + FlacTestCase::Success { + test_name: "subset_48", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac" + ), + in_hash: "4417aca6b5f90971c50c28766d2f32b3acaa7f9f9667bd313336242dae8b2531", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2445, + max_frame_size: 7364, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 259884, + md5_signature: "97a0574290237563fbaa788ad77d2cdf", + }, + FlacBlockOutput::Seektable { + hash: "21ca2184ae22fe26b690fd7cbd8d25fcde1d830ff6e5796ced4107bab219d7c0", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + ], + audio_hash: "c2d691f2c4c986fe3cd5fd7864d9ba9ce6dd68a4ffc670447f008434b13102c2", + stripped_hash: "abc9a0c40a29c896bc6e1cc0b374db1c8e157af716a5a3c43b7db1591a74c4e8", + }, + FlacTestCase::Success { + test_name: "subset_49", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/49 - Extremely large PADDING.flac", + ), + in_hash: "7bc44fa2754536279fde4f8fb31d824f43b8d0b3f93d27d055d209682914f20e", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 1353, + max_frame_size: 7117, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 258939, + md5_signature: "6e78f221caaaa5d570a53f1714d84ded", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Padding { size: 16777215 }, + ], + audio_hash: "5007be7109b28b0149d1b929d2a0e93a087381bd3e68cf2a3ef78ea265ea20c3", + stripped_hash: "a2283bbacbc4905ad3df1bf9f43a0ea7aa65cf69523d84a7dd8eb54553cc437e", + }, + FlacTestCase::Success { + test_name: "subset_50", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/50 - Extremely large PICTURE.flac" + ), + in_hash: "1f04f237d74836104993a8072d4223e84a5d3bd76fbc44555c221c7e69a23594", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 5099, + max_frame_size: 7126, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 265617, + md5_signature: "82164e4da30ed43b47e6027cef050648", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + ], + audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", + stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", + }, + FlacTestCase::Success { + test_name: "subset_51", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac" + ), + in_hash: "033160e8124ed287b0b5d615c94ac4139477e47d6e4059b1c19b7141566f5ef9", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 4531, + max_frame_size: 7528, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 289972, + md5_signature: "5ff622c88f8dd9bc201a6a541f3890d3", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Hash { + n_comments: 39, + hash: "01984e9ec0cfad41f27b3b4e84184966f6725ead84b7815bd0b3313549ee4229", + }, + pictures: &[], + }, + ], + audio_hash: "76419865d10eb22a74f020423a4e515e800f0177441676afd0418557c2d76c36", + stripped_hash: "c0ca6c6099b5d9ec53d6bb370f339b2b1570055813a6cd3616fac2db83a2185e", + }, + FlacTestCase::Success { + test_name: "subset_52", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/52 - Extremely large APPLICATION.flac" + ), + in_hash: "0e45a4f8dbef15cbebdd8dfe690d8ae60e0c6abb596db1270a9161b62a7a3f1c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3711, + max_frame_size: 7056, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 317876, + md5_signature: "eb7140266bc194527488c21ab49bc47b", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Application { + application_id: 0x74657374, + hash: "cfc0b8969e4ba6bd507999ba89dea2d274df69d94749d6ae3cf117a7780bba09", + }, + ], + audio_hash: "89ad1a5c86a9ef35d33189c81c8a90285a23964a13f8325bf2c02043e8c83d63", + stripped_hash: "cc4a0afb95ec9bcde8ee33f13951e494dc4126a9a3a668d79c80ce3c14a3acd9", + }, + FlacTestCase::Success { + test_name: "subset_53", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac" + ), + in_hash: "513fad18578f3225fae5de1bda8f700415be6fd8aa1e7af533b5eb796ed2d461", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2798, + max_frame_size: 7408, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 2910025, + md5_signature: "d11f3717d628cfe6a90a10facc478340", + }, + FlacBlockOutput::Seektable { + hash: "18629e1b874cb27e4364da72fb3fec2141eb0618baae4a1cee6ed09562aa00a8", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::CueSheet { + hash: "70638a241ca06881a52c0a18258ea2d8946a830137a70479c49746d2a1344bdd", + }, + ], + audio_hash: "e993070f2080f2c598be1d61d208e9187a55ddea4be1d2ed1f8043e7c03e97a5", + stripped_hash: "57c5b945e14c6fcd06916d6a57e5b036d67ff35757893c24ed872007aabbcf4b", + }, + FlacTestCase::Success { + test_name: "subset_54", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac" + ), + in_hash: "b68dc6644784fac35aa07581be8603a360d1697e07a2265d7eb24001936fd247", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 1694, + max_frame_size: 7145, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 433151, + md5_signature: "1d950e92b357dedbc5290a7f2210a2ef", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Hash { + n_comments: 20000, + hash: "433f34ae532d265835153139b1db79352a26ad0d3b03e2f1a1b88ada34abfc77", + }, + pictures: &[], + }, + ], + audio_hash: "4721b784058410c6263f73680079e9a71aee914c499afcf5580c121fce00e874", + stripped_hash: "5c8b92b83c0fa17821add38263fa323d1c66cfd2ee57aca054b50bd05b9df5c2", + }, + FlacTestCase::Success { + test_name: "subset_55", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/55 - file 48-53 combined.flac" + ), + in_hash: "a756b460df79b7cc492223f80cda570e4511f2024e5fa0c4d505ba51b86191f6", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3103, + max_frame_size: 11306, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 2646000, + md5_signature: "2c78978cbbff11daac296fee97c3e061", + }, + FlacBlockOutput::Seektable { + hash: "58dfa7bac4974edf1956b068f5aa72d1fbd9301c36a3085a8a57b9db11a2dbf0", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.3 20190804", + comments: VorbisCommentTestValue::Hash { + n_comments: 40036, + hash: "66cac9f9c42f48128e9fc24e1e96b46a06e885d233155556da16d9b05a23486e", + }, + pictures: &[], + }, + FlacBlockOutput::CueSheet { + hash: "db11916c8f5f39648256f93f202e00ff8d73d7d96b62f749b4c77cf3ea744f90", + }, + FlacBlockOutput::Application { + application_id: 0x74657374, + hash: "6088a557a1bad7bfa5ebf79a324669fbf4fa2f8e708f5487305dfc5b2ff2249a", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + FlacBlockOutput::Padding { size: 16777215 }, + ], + audio_hash: "f1285b77cec7fa9a0979033244489a9d06b8515b2158e9270087a65a4007084d", + stripped_hash: "401038fce06aff5ebdc7a5f2fc01fa491cbf32d5da9ec99086e414b2da3f8449", + }, + FlacTestCase::Success { + test_name: "subset_56", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/56 - JPG PICTURE.flac" + ), + in_hash: "5cebe7a3710cf8924bd2913854e9ca60b4cd53cfee5a3af0c3c73fddc1888963", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 3014, + max_frame_size: 7219, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 220026, + md5_signature: "5b0e898d9c2626d0c28684f5a586813f", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", + }, + ], + audio_hash: "ccfe90b0f15cd9662f7a18f40cd4c347538cf8897a08228e75351206f7804573", + stripped_hash: "31a38d59db2010790b7abf65ec0cc03f2bbe1fed5952bc72bee4ca4d0c92e79f", + }, + FlacTestCase::Success { + test_name: "subset_57", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/57 - PNG PICTURE.flac" + ), + in_hash: "c6abff7f8bb63c2821bd21dd9052c543f10ba0be878e83cb419c248f14f72697", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 463, + max_frame_size: 6770, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221623, + md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }, + ], + audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", + stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", + }, + FlacTestCase::Success { + test_name: "subset_58", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/58 - GIF PICTURE.flac" + ), + in_hash: "7c2b1a963a665847167a7275f9924f65baeb85c21726c218f61bf3f803f301c8", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 2853, + max_frame_size: 6683, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 219826, + md5_signature: "7c1810602a7db96d7a48022ac4aa495c", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_GIF, + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 32, + img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", + }, + ], + audio_hash: "30e3292e9f56cf88658eeadfdec8ad3a440690ce6d813e1b3374f60518c8e0ae", + stripped_hash: "4cd771e27870e2a586000f5b369e0426183a521b61212302a2f5802b046910b2", + }, + FlacTestCase::Success { + test_name: "subset_59", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_subset/59 - AVIF PICTURE.flac" + ), + in_hash: "7395d02bf8d9533dc554cce02dee9de98c77f8731a45f62d0a243bd0d6f9a45c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 153, + max_frame_size: 7041, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221423, + md5_signature: "d354246011ca204159c06f52cad5f634", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: Mime::from_str("image/avif").unwrap(), + description: "", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", + }, + ], + audio_hash: "b208c73d274e65b27232bfffbfcbcf4805ee3cbc9cfbf7d2104db8f53370273b", + stripped_hash: "d5215e16c6b978fc2c3e6809e1e78981497cb8514df297c5169f3b4a28fd875c", + }, + FlacTestCase::Success { + test_name: "custom_01", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/01 - many images.flac" + ), + in_hash: "8a5df37488866cd91ac16773e549ef4e3a85d9f88a0d9d345f174807bb536b96", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 5099, + max_frame_size: 7126, + sample_rate: 48000, + channels: 2, + bits_per_sample: 16, + total_samples: 265617, + md5_signature: "82164e4da30ed43b47e6027cef050648", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }], + }, + FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_JPEG, + description: "", + width: 3200, + height: 2252, + bit_depth: 24, + color_count: 0, + img_data: "b78c3a48fde4ebbe8e4090e544caeb8f81ed10020d57cc50b3265f9b338d8563", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::ABrightColoredFish, + mime: mime::IMAGE_JPEG, + description: "lorem", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "7a3ed658f80f433eee3914fff451ea0312807de0af709e37cc6a4f3f6e8a47c6", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::OtherFileIcon, + mime: mime::IMAGE_PNG, + description: "ipsum", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::Lyricist, + mime: mime::IMAGE_GIF, + description: "dolor", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 32, + img_data: "e33cccc1d799eb2bb618f47be7099cf02796df5519f3f0e1cc258606cf6e8bb1", + }, + FlacBlockOutput::Picture { + picture_type: PictureType::BackCover, + mime: Mime::from_str("image/avif").unwrap(), + description: "est", + width: 1920, + height: 1080, + bit_depth: 24, + color_count: 0, + img_data: "a431123040c74f75096237f20544a7fb56b4eb71ddea62efa700b0a016f5b2fc", + }, + ], + audio_hash: "9778b25c5d1f56cfcd418e550baed14f9d6a4baf29489a83ed450fbebb28de8c", + stripped_hash: "20df129287d94f9ae5951b296d7f65fcbed92db423ba7db4f0d765f1f0a7e18c", + }, + FlacTestCase::Success { + test_name: "custom_02", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/02 - picture in vorbis comment.flac" + ), + in_hash: "f6bb1a726fe6a3e25a4337d36e29fdced8ff01a46d627b7c2e1988c88f461f8c", + blocks: vec![ + FlacBlockOutput::Streaminfo { + min_block_size: 4096, + max_block_size: 4096, + min_frame_size: 463, + max_frame_size: 6770, + sample_rate: 44100, + channels: 2, + bits_per_sample: 16, + total_samples: 221623, + md5_signature: "ad16957bcf8d5a3ec8caf261e43d5ff7", + }, + FlacBlockOutput::VorbisComment { + vendor: "reference libFLAC 1.3.2 20170101", + comments: VorbisCommentTestValue::Raw { tags: &[] }, + pictures: &[FlacBlockOutput::Picture { + picture_type: PictureType::FrontCover, + mime: mime::IMAGE_PNG, + description: "", + width: 960, + height: 540, + bit_depth: 24, + color_count: 0, + img_data: "d804e5c7b9ee5af694b5e301c6cdf64508ff85997deda49d2250a06a964f10b2", + }], + }, + ], + audio_hash: "39bf9981613ac2f35d253c0c21b76a48abba7792c27da5dbf23e6021e2e6673f", + stripped_hash: "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", + }, + FlacTestCase::Error { + test_name: "custom_03", + file_path: concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac" + ), + in_hash: "7177f0ae4f04a563292be286ec05967f81ab16eb0a28b70fc07a1e47da9cafd0", + check_error: &|x| { + matches!( + x, + FlacDecodeError::VorbisComment(VorbisCommentDecodeError::MalformedPicture) + ) + }, + stripped_hash: Some( + "3328201dd56289b6c81fa90ff26cb57fa9385cb0db197e89eaaa83efd79a58b1", + ), + pictures: None, + }, + ] + } + + #[test] + fn manifest_sanity_check() { + assert!(manifest().iter().map(|x| x.get_name()).all_unique()); + assert!(manifest().iter().map(|x| x.get_path()).all_unique()); + } +} diff --git a/crates/pile-audio/src/flac/proc/metastrip.rs b/crates/pile-audio/src/flac/proc/metastrip.rs new file mode 100644 index 0000000..2f51f75 --- /dev/null +++ b/crates/pile-audio/src/flac/proc/metastrip.rs @@ -0,0 +1,208 @@ +//! A flac processor that strips metadata blocks from flac files + +use std::io::Write; + +use super::super::{ + blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector}, + errors::FlacEncodeError, +}; + +/// Removes all metadata from a flac file +pub struct FlacMetaStrip { + reader: FlacBlockReader, + + /// The last block that `reader` produced. + /// + /// We need this to detect the last metadata block + /// that `reader` produces. + last_block: Option, + + /// Set to `false` on the first call to `self.write_data`. + /// Used to write fLaC magic bytes. + first_write: bool, +} + +impl FlacMetaStrip { + /// Make a new [`FlacMetaStrip`] + pub fn new() -> Self { + Self { + first_write: true, + last_block: None, + reader: FlacBlockReader::new(FlacBlockSelector { + pick_streaminfo: true, + pick_padding: false, + pick_application: false, + pick_seektable: true, + pick_vorbiscomment: false, + pick_cuesheet: true, + pick_picture: false, + pick_audio: true, + }), + } + } + + /// Push some data to this flac processor + pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { + self.reader.push_data(buf) + } + + /// Call after sending the entire flac file to this reader + pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { + self.reader.finish() + } + + /// If true, we have received all the data we need + pub fn is_done(&mut self) -> bool { + self.reader.is_done() + } + + /// If false, this reader has sent all its data. + /// + /// Note that `read_data` may write zero bytes if this method returns `true`. + /// If `has_data` is false, we don't AND WON'T have data. If we're waiting + /// for data, this is `true`. + pub fn has_data(&self) -> bool { + self.last_block.is_some() || !self.reader.is_done() || self.reader.has_block() + } + + /// Write available data from this struct into `target` + pub fn read_data(&mut self, target: &mut impl Write) -> Result<(), FlacEncodeError> { + if self.first_write { + target.write_all(&[0x66, 0x4C, 0x61, 0x43])?; + self.first_write = false; + } + + while let Some(block) = self.reader.pop_block() { + if let Some(last_block) = self.last_block.take() { + last_block.encode( + // The last metadata block is the only one followed by an audio frame + !matches!(last_block, FlacBlock::AudioFrame(_)) + && matches!(block, FlacBlock::AudioFrame(_)), + true, + target, + )?; + } + self.last_block = Some(block); + } + + // We don't need to store audioframes in our last_block buffer, + // since they do not have an `is_last` flag. + if matches!(self.last_block, Some(FlacBlock::AudioFrame(_))) { + let x = self.last_block.take().unwrap(); + x.encode(false, true, target)?; + } + + return Ok(()); + } +} + +#[cfg(test)] +mod tests { + use paste::paste; + use rand::Rng; + use sha2::{Digest, Sha256}; + + use crate::flac::{ + blockread::FlacBlockReaderError, proc::metastrip::FlacMetaStrip, tests::FlacTestCase, + tests::manifest, + }; + + fn test_strip( + test_case: &FlacTestCase, + fragment_size_range: Option>, + ) -> Result<(), FlacBlockReaderError> { + let file_data = std::fs::read(test_case.get_path()).unwrap(); + + // Make sure input file is correct + let mut hasher = Sha256::new(); + hasher.update(&file_data); + assert_eq!( + test_case.get_in_hash(), + hasher.finalize().map(|x| format!("{x:02x}")).join("") + ); + + let mut strip = FlacMetaStrip::new(); + + // Push file data to the reader, in parts or as a whole. + if let Some(fragment_size_range) = fragment_size_range { + let mut head = 0; + while head < file_data.len() { + let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); + if head + frag_size > file_data.len() { + frag_size = file_data.len() - head; + } + strip.push_data(&file_data[head..head + frag_size])?; + head += frag_size; + } + } else { + strip.push_data(&file_data)?; + } + + strip.finish()?; + + let mut out_data = Vec::new(); + strip.read_data(&mut out_data).unwrap(); + assert!(!strip.has_data()); + + let mut hasher = Sha256::new(); + hasher.update(&out_data); + let result = hasher.finalize().map(|x| format!("{x:02x}")).join(""); + assert_eq!( + result, + test_case.get_stripped_hash().unwrap(), + "Stripped FLAC hash doesn't match" + ); + + return Ok(()); + } + + macro_rules! gen_tests { + ( $test_name:ident ) => { + paste! { + #[test] + pub fn []() { + let manifest =manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + match test_case { + FlacTestCase::Error { stripped_hash: Some(_), .. } | + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_strip( + test_case, + Some(5_000..100_000), + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_strip(test_case, Some(5_000..100_000)).unwrap_err(); + match e { + FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + } + }; + + + } + + gen_tests!(custom_01); + gen_tests!(custom_02); + gen_tests!(custom_03); + + gen_tests!(uncommon_10); + + gen_tests!(faulty_06); + gen_tests!(faulty_07); + gen_tests!(faulty_10); + gen_tests!(faulty_11); + + gen_tests!(subset_45); + gen_tests!(subset_47); + gen_tests!(subset_54); + gen_tests!(subset_55); + gen_tests!(subset_57); +} diff --git a/crates/pile-audio/src/flac/proc/mod.rs b/crates/pile-audio/src/flac/proc/mod.rs new file mode 100644 index 0000000..24bf102 --- /dev/null +++ b/crates/pile-audio/src/flac/proc/mod.rs @@ -0,0 +1,5 @@ +//! Flac processors. These are well-tested wrappers around [`crate::flac::blockread::FlacBlockReader`] +//! that are specialized for specific tasks. + +pub mod metastrip; +pub mod pictures; diff --git a/crates/pile-audio/src/flac/proc/pictures.rs b/crates/pile-audio/src/flac/proc/pictures.rs new file mode 100644 index 0000000..9661f49 --- /dev/null +++ b/crates/pile-audio/src/flac/proc/pictures.rs @@ -0,0 +1,228 @@ +//! A flac processor that finds all images inside a flac file + +use std::collections::VecDeque; + +use super::super::{ + blockread::{FlacBlock, FlacBlockReader, FlacBlockReaderError, FlacBlockSelector}, + blocks::FlacPictureBlock, +}; + +/// Find all pictures in a flac file, +/// in both picture metablocks and vorbis comments. +pub struct FlacPictureReader { + reader: FlacBlockReader, + pictures: VecDeque, +} + +impl FlacPictureReader { + /// Make a new [`FlacMetaStrip`] + pub fn new() -> Self { + Self { + pictures: VecDeque::new(), + reader: FlacBlockReader::new(FlacBlockSelector { + pick_streaminfo: false, + pick_padding: false, + pick_application: false, + pick_seektable: false, + pick_vorbiscomment: true, + pick_cuesheet: false, + pick_picture: true, + pick_audio: false, + }), + } + } + + /// Push some data to this flac processor + pub fn push_data(&mut self, buf: &[u8]) -> Result<(), FlacBlockReaderError> { + self.reader.push_data(buf)?; + + while let Some(b) = self.reader.pop_block() { + match b { + FlacBlock::Picture(p) => self.pictures.push_back(p), + + FlacBlock::VorbisComment(c) => { + for p in c.comment.pictures { + self.pictures.push_back(p) + } + } + + _ => unreachable!(), + } + } + + return Ok(()); + } + + /// Call after sending the entire flac file to this reader + pub fn finish(&mut self) -> Result<(), FlacBlockReaderError> { + self.reader.finish() + } + + /// If true, we have received all the data we need + pub fn is_done(&mut self) -> bool { + self.reader.is_done() + } + + /// If false, this reader has sent all its data. + /// + /// Note that `read_data` may write zero bytes if this method returns `true`. + /// If `has_data` is false, we don't AND WON'T have data. If we're waiting + /// for data, this is `true`. + pub fn has_data(&self) -> bool { + !self.reader.is_done() || self.reader.has_block() || !self.pictures.is_empty() + } + + /// Pop the next picture we read from this file, if any. + pub fn pop_picture(&mut self) -> Option { + self.pictures.pop_front() + } +} + +#[cfg(test)] +mod tests { + use paste::paste; + use rand::Rng; + use sha2::{Digest, Sha256}; + + use crate::flac::{ + blockread::FlacBlockReaderError, + proc::pictures::FlacPictureReader, + tests::{FlacBlockOutput, FlacTestCase, manifest}, + }; + + fn test_pictures( + test_case: &FlacTestCase, + fragment_size_range: Option>, + ) -> Result<(), FlacBlockReaderError> { + let file_data = std::fs::read(test_case.get_path()).unwrap(); + + // Make sure input file is correct + let mut hasher = Sha256::new(); + hasher.update(&file_data); + assert_eq!( + test_case.get_in_hash(), + hasher.finalize().map(|x| format!("{x:02x}")).join("") + ); + + let mut pic = FlacPictureReader::new(); + + // Push file data to the reader, in parts or as a whole. + if let Some(fragment_size_range) = fragment_size_range { + let mut head = 0; + while head < file_data.len() { + let mut frag_size = rand::rng().random_range(fragment_size_range.clone()); + if head + frag_size > file_data.len() { + frag_size = file_data.len() - head; + } + pic.push_data(&file_data[head..head + frag_size])?; + head += frag_size; + } + } else { + pic.push_data(&file_data)?; + } + + pic.finish()?; + + let mut out = Vec::new(); + while let Some(p) = pic.pop_picture() { + out.push(p); + } + + let out_pictures = test_case.get_pictures().unwrap(); + + assert_eq!( + out.len(), + out_pictures.len(), + "Unexpected number of pictures" + ); + + for (got, expected) in out.iter().zip(out_pictures) { + let (picture_type, mime, description, width, height, bit_depth, color_count, img_data) = + match expected { + FlacBlockOutput::Picture { + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + } => ( + picture_type, + mime, + description, + width, + height, + bit_depth, + color_count, + img_data, + ), + _ => unreachable!(), + }; + + assert_eq!(picture_type, got.picture_type, "{}", test_case.get_name()); + assert_eq!(mime, got.mime, "{}", test_case.get_name()); + assert_eq!(*description, got.description, "{}", test_case.get_name()); + assert_eq!(width, got.width, "{}", test_case.get_name()); + assert_eq!(height, got.height, "{}", test_case.get_name()); + assert_eq!(bit_depth, got.bit_depth, "{}", test_case.get_name()); + assert_eq!(color_count, got.color_count, "{}", test_case.get_name()); + assert_eq!( + *img_data, + { + let mut hasher = Sha256::new(); + hasher.update(&got.img_data); + hasher.finalize().map(|x| format!("{x:02x}")).join("") + }, + "{}", + test_case.get_name() + ); + } + + return Ok(()); + } + + macro_rules! gen_tests { + ( $test_name:ident ) => { + paste! { + #[test] + pub fn []() { + let manifest = manifest(); + let test_case = manifest.iter().find(|x| x.get_name() == stringify!($test_name)).unwrap(); + match test_case { + FlacTestCase::Error { pictures: Some(_), .. } | + FlacTestCase::Success { .. } => { + for _ in 0..5 { + test_pictures( + test_case, + Some(5_000..100_000), + ).unwrap() + } + }, + + FlacTestCase::Error { check_error, .. } => { + let e = test_pictures(test_case, Some(5_000..100_000)).unwrap_err(); + match e { + FlacBlockReaderError::DecodeError(e) => assert!(check_error(&e), "Unexpected error {e:?}"), + _ => panic!("Unexpected error {e:?}") + } + } + } + } + } + }; + } + + gen_tests!(custom_01); + gen_tests!(custom_02); + gen_tests!(custom_03); + + gen_tests!(subset_47); + gen_tests!(subset_50); + gen_tests!(subset_55); + gen_tests!(subset_56); + gen_tests!(subset_57); + gen_tests!(subset_58); + gen_tests!(subset_59); +} diff --git a/crates/pile-audio/src/lib.rs b/crates/pile-audio/src/lib.rs new file mode 100644 index 0000000..7f10084 --- /dev/null +++ b/crates/pile-audio/src/lib.rs @@ -0,0 +1,3 @@ +//! Read and write audio file metadata. +pub mod common; +pub mod flac; diff --git a/crates/pile-audio/tests/files/README.md b/crates/pile-audio/tests/files/README.md new file mode 100644 index 0000000..ff7d5bb --- /dev/null +++ b/crates/pile-audio/tests/files/README.md @@ -0,0 +1,13 @@ +# Audio files for tests + +## Sources +- `./flac_subset`: Files from the "subset" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main) +- `./flac_faulty`: Files from the "faulty" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main) +- `./flac_uncommon`: Files from the "uncommon" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main) +- `./flac_custom`: Custom files based on [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main) + + + + + + diff --git a/crates/pile-audio/tests/files/flac_custom/01 - many images.flac b/crates/pile-audio/tests/files/flac_custom/01 - many images.flac new file mode 100644 index 0000000..ba88df1 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_custom/01 - many images.flac differ diff --git a/crates/pile-audio/tests/files/flac_custom/02 - picture in vorbis comment.flac b/crates/pile-audio/tests/files/flac_custom/02 - picture in vorbis comment.flac new file mode 100644 index 0000000..a49e504 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_custom/02 - picture in vorbis comment.flac differ diff --git a/crates/pile-audio/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac b/crates/pile-audio/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac new file mode 100644 index 0000000..702d43b Binary files /dev/null and b/crates/pile-audio/tests/files/flac_custom/03 - faulty picture in vorbis comment.flac differ diff --git a/crates/pile-audio/tests/files/flac_custom/LICENSE.txt b/crates/pile-audio/tests/files/flac_custom/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_custom/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/crates/pile-audio/tests/files/flac_custom/README.md b/crates/pile-audio/tests/files/flac_custom/README.md new file mode 100644 index 0000000..00675c1 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_custom/README.md @@ -0,0 +1,17 @@ +# Custom FLAC test files + +These are flac files created specifically for Copper, and test cases that the flac test toolkit doesn't cover. +Most of these are modified copies of files in `flac_subset`, `flac_faulty`, or `flac_uncommon` + + +## Manifest + +- `01 - many images.flac`: This is `flac_subset/50` with additional images from `56`, `57`, `58`, and `59`, in that order. + - Image 0: from file `50`, type is `3`, description is empty. + - Image 1: from file `56`, type is `17`, description is `lorem`. + - Image 2: from file `57`, type is `2`, description is `ipsum`. + - Image 3: from file `58`, type is `12`, description is `dolor`. + - Image 4: from file `59`, type is `4`, description is `est`. + - Image `57` is also stored in the vorbis comment as a `METADATA_BLOCK_PICTURE`. +- `02 - picture in vorbis comment.flac`: This is `flac_subset/57`, but with the image stored inside a vorbis `METADATA_BLOCK_PICTURE` comment instead of a proper flac picture metablock. +- `03 - faulty picture in vorbis comment.flac`: This is `02`, but with a corrupt picture. diff --git a/crates/pile-audio/tests/files/flac_faulty/01 - wrong max blocksize.flac b/crates/pile-audio/tests/files/flac_faulty/01 - wrong max blocksize.flac new file mode 100644 index 0000000..9384699 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/01 - wrong max blocksize.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/02 - wrong maximum framesize.flac b/crates/pile-audio/tests/files/flac_faulty/02 - wrong maximum framesize.flac new file mode 100644 index 0000000..5b006c6 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/02 - wrong maximum framesize.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/03 - wrong bit depth.flac b/crates/pile-audio/tests/files/flac_faulty/03 - wrong bit depth.flac new file mode 100644 index 0000000..564b792 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/03 - wrong bit depth.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/04 - wrong number of channels.flac b/crates/pile-audio/tests/files/flac_faulty/04 - wrong number of channels.flac new file mode 100644 index 0000000..66ba03d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/04 - wrong number of channels.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/05 - wrong total number of samples.flac b/crates/pile-audio/tests/files/flac_faulty/05 - wrong total number of samples.flac new file mode 100644 index 0000000..974f6d9 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/05 - wrong total number of samples.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac b/crates/pile-audio/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac new file mode 100644 index 0000000..f681fd8 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/06 - missing streaminfo metadata block.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac b/crates/pile-audio/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac new file mode 100644 index 0000000..10d55c9 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/07 - other metadata blocks preceding streaminfo metadata block.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/08 - blocksize 65536.flac b/crates/pile-audio/tests/files/flac_faulty/08 - blocksize 65536.flac new file mode 100644 index 0000000..93afea0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/08 - blocksize 65536.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac b/crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac new file mode 100644 index 0000000..f165d12 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac b/crates/pile-audio/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac new file mode 100644 index 0000000..e13aaf5 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/10 - invalid vorbis comment metadata block.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/11 - incorrect metadata block length.flac b/crates/pile-audio/tests/files/flac_faulty/11 - incorrect metadata block length.flac new file mode 100644 index 0000000..04989f4 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_faulty/11 - incorrect metadata block length.flac differ diff --git a/crates/pile-audio/tests/files/flac_faulty/LICENSE.txt b/crates/pile-audio/tests/files/flac_faulty/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_faulty/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/crates/pile-audio/tests/files/flac_faulty/README.md b/crates/pile-audio/tests/files/flac_faulty/README.md new file mode 100644 index 0000000..610ee43 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_faulty/README.md @@ -0,0 +1,62 @@ +# Group faulty + +This group contains files with invalid data and corrupted files +that might trigger crashes in a decoder. A decoder should not +crash or freeze reading these files. Read the README.txt in +the directory faulty for details on each file. + + +## Manifest + +- File 01 has a streaminfo metadata block that lists the wrong + maximum blocksize for the file. The file has a fixed + blocksize of 16384 samples, but the streaminfo metadata block + says the maximum blocksize is 4096. When a decoder + initializes buffers for a blocksize of 4096 and tries to + decode a block with 16384 samples, it might overrun a buffer + +- File 02 has a streaminfo metadata block that lists the wrong + maximum framesize for the file. The file has an actual + maximum framesize of 8846 byte, but the streaminfo metadata + block says the maximum framesize is 654 byte. When a decoder + initializes buffers for a frames of at most 654 byte and + tries to read a frame of 8846 byte, it might overrun a buffer + +- File 03 has a streaminfo metadata block that lists the wrong + bit depth for the file. It says the bit depth is 24, but the + actual bit depth of all frames is 16. + +- File 04 has a streaminfo metadata block that lists the wrong + number of channels for the file. It says the number of + channels is 5, but the actual number of channels is 1. + +- File 05 has a streaminfo metadata block that lists the wrong + total number of samples. It says the number of samples is + 39842, while the actual total number of samples is 109487 + +- File 06 doesn't have a streaminfo metadata block, despite + having other metadata blocks, unlike the files 10 and 11 in + the 'uncommon' set of files, which start directly at a frame + header. It does have two other metadata blocks, a vorbis + comment block and a padding block + +- File 07 has a streaminfo metadata block that is not the first + metadata block of the file. It is being preceded by two other + metadata blocks, a vorbis comment block and a padding block + +- File 08 has a blocksize of 65536, which is representable by + the frame header but not by the streaminfo metadata block, + and is thus invalid + +- File 09 has a blocksize of 1, which is not allowed for all + but the last frame of a file + +- File 10 has a vorbis comment metadata block with invalid + contents. The block has a correct overall length and has a + single tag, but it states there are 10 tags, which means a + parser could overread the block if it does not check for + validity. + +- File 11 has an incorrect metadata block length, which leads + to the parser searching for the next metadata block reading + garbage. diff --git a/crates/pile-audio/tests/files/flac_subset/01 - blocksize 4096.flac b/crates/pile-audio/tests/files/flac_subset/01 - blocksize 4096.flac new file mode 100644 index 0000000..0eca2c9 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/01 - blocksize 4096.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/02 - blocksize 4608.flac b/crates/pile-audio/tests/files/flac_subset/02 - blocksize 4608.flac new file mode 100644 index 0000000..dfbe3fc Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/02 - blocksize 4608.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac b/crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac new file mode 100644 index 0000000..5ce3926 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/04 - blocksize 192.flac b/crates/pile-audio/tests/files/flac_subset/04 - blocksize 192.flac new file mode 100644 index 0000000..05475d2 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/04 - blocksize 192.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/05 - blocksize 254.flac b/crates/pile-audio/tests/files/flac_subset/05 - blocksize 254.flac new file mode 100644 index 0000000..84da704 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/05 - blocksize 254.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/06 - blocksize 512.flac b/crates/pile-audio/tests/files/flac_subset/06 - blocksize 512.flac new file mode 100644 index 0000000..38c3493 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/06 - blocksize 512.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/07 - blocksize 725.flac b/crates/pile-audio/tests/files/flac_subset/07 - blocksize 725.flac new file mode 100644 index 0000000..1522a3e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/07 - blocksize 725.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/08 - blocksize 1000.flac b/crates/pile-audio/tests/files/flac_subset/08 - blocksize 1000.flac new file mode 100644 index 0000000..d0bff01 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/08 - blocksize 1000.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/09 - blocksize 1937.flac b/crates/pile-audio/tests/files/flac_subset/09 - blocksize 1937.flac new file mode 100644 index 0000000..b4fa7fd Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/09 - blocksize 1937.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/10 - blocksize 2304.flac b/crates/pile-audio/tests/files/flac_subset/10 - blocksize 2304.flac new file mode 100644 index 0000000..3e327b0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/10 - blocksize 2304.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/11 - partition order 8.flac b/crates/pile-audio/tests/files/flac_subset/11 - partition order 8.flac new file mode 100644 index 0000000..941862e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/11 - partition order 8.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/12 - qlp precision 15 bit.flac b/crates/pile-audio/tests/files/flac_subset/12 - qlp precision 15 bit.flac new file mode 100644 index 0000000..90b31db Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/12 - qlp precision 15 bit.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/13 - qlp precision 2 bit.flac b/crates/pile-audio/tests/files/flac_subset/13 - qlp precision 2 bit.flac new file mode 100644 index 0000000..37047d5 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/13 - qlp precision 2 bit.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac b/crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac new file mode 100644 index 0000000..3ab0e77 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/15 - only verbatim subframes.flac b/crates/pile-audio/tests/files/flac_subset/15 - only verbatim subframes.flac new file mode 100644 index 0000000..3709364 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/15 - only verbatim subframes.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac b/crates/pile-audio/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac new file mode 100644 index 0000000..c54576b Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/16 - partition order 8 containing escaped partitions.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/17 - all fixed orders.flac b/crates/pile-audio/tests/files/flac_subset/17 - all fixed orders.flac new file mode 100644 index 0000000..398b026 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/17 - all fixed orders.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/18 - precision search.flac b/crates/pile-audio/tests/files/flac_subset/18 - precision search.flac new file mode 100644 index 0000000..13ead51 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/18 - precision search.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/19 - samplerate 35467Hz.flac b/crates/pile-audio/tests/files/flac_subset/19 - samplerate 35467Hz.flac new file mode 100644 index 0000000..4f08d3f Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/19 - samplerate 35467Hz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/20 - samplerate 39kHz.flac b/crates/pile-audio/tests/files/flac_subset/20 - samplerate 39kHz.flac new file mode 100644 index 0000000..c471d4d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/20 - samplerate 39kHz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/21 - samplerate 22050Hz.flac b/crates/pile-audio/tests/files/flac_subset/21 - samplerate 22050Hz.flac new file mode 100644 index 0000000..060bb8a Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/21 - samplerate 22050Hz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/22 - 12 bit per sample.flac b/crates/pile-audio/tests/files/flac_subset/22 - 12 bit per sample.flac new file mode 100644 index 0000000..137aae0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/22 - 12 bit per sample.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/23 - 8 bit per sample.flac b/crates/pile-audio/tests/files/flac_subset/23 - 8 bit per sample.flac new file mode 100644 index 0000000..a363913 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/23 - 8 bit per sample.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac b/crates/pile-audio/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac new file mode 100644 index 0000000..ea64d9a Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/24 - variable blocksize file created with flake revision 264.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac b/crates/pile-audio/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac new file mode 100644 index 0000000..5830d15 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/25 - variable blocksize file created with flake revision 264, modified to create smaller blocks.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac b/crates/pile-audio/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac new file mode 100644 index 0000000..fc494b9 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/26 - variable blocksize file created with CUETools.Flake 2.1.6.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac b/crates/pile-audio/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac new file mode 100644 index 0000000..920885e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/27 - old format variable blocksize file created with Flake 0.11.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/28 - high resolution audio, default settings.flac b/crates/pile-audio/tests/files/flac_subset/28 - high resolution audio, default settings.flac new file mode 100644 index 0000000..4adc001 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/28 - high resolution audio, default settings.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac b/crates/pile-audio/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac new file mode 100644 index 0000000..022de6f Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/29 - high resolution audio, blocksize 16384.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac b/crates/pile-audio/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac new file mode 100644 index 0000000..6fee2dd Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/30 - high resolution audio, blocksize 13456.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac b/crates/pile-audio/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac new file mode 100644 index 0000000..c13449e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/31 - high resolution audio, using only 32nd order predictors.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac b/crates/pile-audio/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac new file mode 100644 index 0000000..a2611f0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/32 - high resolution audio, partition order 8 containing escaped partitions.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/33 - samplerate 192kHz.flac b/crates/pile-audio/tests/files/flac_subset/33 - samplerate 192kHz.flac new file mode 100644 index 0000000..e2f0a4d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/33 - samplerate 192kHz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac b/crates/pile-audio/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac new file mode 100644 index 0000000..0330593 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/34 - samplerate 192kHz, using only 32nd order predictors.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/35 - samplerate 134560Hz.flac b/crates/pile-audio/tests/files/flac_subset/35 - samplerate 134560Hz.flac new file mode 100644 index 0000000..50d4ad7 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/35 - samplerate 134560Hz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/36 - samplerate 384kHz.flac b/crates/pile-audio/tests/files/flac_subset/36 - samplerate 384kHz.flac new file mode 100644 index 0000000..9c4884c Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/36 - samplerate 384kHz.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/37 - 20 bit per sample.flac b/crates/pile-audio/tests/files/flac_subset/37 - 20 bit per sample.flac new file mode 100644 index 0000000..c137fc3 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/37 - 20 bit per sample.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/38 - 3 channels (3.0).flac b/crates/pile-audio/tests/files/flac_subset/38 - 3 channels (3.0).flac new file mode 100644 index 0000000..bf75b7e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/38 - 3 channels (3.0).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/39 - 4 channels (4.0).flac b/crates/pile-audio/tests/files/flac_subset/39 - 4 channels (4.0).flac new file mode 100644 index 0000000..61d43ef Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/39 - 4 channels (4.0).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/40 - 5 channels (5.0).flac b/crates/pile-audio/tests/files/flac_subset/40 - 5 channels (5.0).flac new file mode 100644 index 0000000..de82c4f Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/40 - 5 channels (5.0).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/41 - 6 channels (5.1).flac b/crates/pile-audio/tests/files/flac_subset/41 - 6 channels (5.1).flac new file mode 100644 index 0000000..dfbd28d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/41 - 6 channels (5.1).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/42 - 7 channels (6.1).flac b/crates/pile-audio/tests/files/flac_subset/42 - 7 channels (6.1).flac new file mode 100644 index 0000000..43028e3 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/42 - 7 channels (6.1).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/43 - 8 channels (7.1).flac b/crates/pile-audio/tests/files/flac_subset/43 - 8 channels (7.1).flac new file mode 100644 index 0000000..61985e0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/43 - 8 channels (7.1).flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac b/crates/pile-audio/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac new file mode 100644 index 0000000..7dbef52 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/44 - 8-channel surround, 192kHz, 24 bit, using only 32nd order predictors.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/45 - no total number of samples set.flac b/crates/pile-audio/tests/files/flac_subset/45 - no total number of samples set.flac new file mode 100644 index 0000000..e139575 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/45 - no total number of samples set.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/46 - no min-max framesize set.flac b/crates/pile-audio/tests/files/flac_subset/46 - no min-max framesize set.flac new file mode 100644 index 0000000..257c747 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/46 - no min-max framesize set.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/47 - only STREAMINFO.flac b/crates/pile-audio/tests/files/flac_subset/47 - only STREAMINFO.flac new file mode 100644 index 0000000..6c23ecc Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/47 - only STREAMINFO.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac b/crates/pile-audio/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac new file mode 100644 index 0000000..4284389 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/48 - Extremely large SEEKTABLE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/49 - Extremely large PADDING.flac b/crates/pile-audio/tests/files/flac_subset/49 - Extremely large PADDING.flac new file mode 100644 index 0000000..a2a393e Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/49 - Extremely large PADDING.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/50 - Extremely large PICTURE.flac b/crates/pile-audio/tests/files/flac_subset/50 - Extremely large PICTURE.flac new file mode 100644 index 0000000..f355fff Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/50 - Extremely large PICTURE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac b/crates/pile-audio/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac new file mode 100644 index 0000000..df59ea0 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/51 - Extremely large VORBISCOMMENT.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/52 - Extremely large APPLICATION.flac b/crates/pile-audio/tests/files/flac_subset/52 - Extremely large APPLICATION.flac new file mode 100644 index 0000000..8f56c66 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/52 - Extremely large APPLICATION.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac b/crates/pile-audio/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac new file mode 100644 index 0000000..806a15a Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/53 - CUESHEET with very many indexes.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac b/crates/pile-audio/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac new file mode 100644 index 0000000..3a18974 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/54 - 1000x repeating VORBISCOMMENT.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/55 - file 48-53 combined.flac b/crates/pile-audio/tests/files/flac_subset/55 - file 48-53 combined.flac new file mode 100644 index 0000000..73fb750 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/55 - file 48-53 combined.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac b/crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac new file mode 100644 index 0000000..78594af Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac b/crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac new file mode 100644 index 0000000..5c15455 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac b/crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac new file mode 100644 index 0000000..7049268 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac b/crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac new file mode 100644 index 0000000..757ee78 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/60 - mono audio.flac b/crates/pile-audio/tests/files/flac_subset/60 - mono audio.flac new file mode 100644 index 0000000..9ca8a67 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/60 - mono audio.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac b/crates/pile-audio/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac new file mode 100644 index 0000000..47884ed Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/61 - predictor overflow check, 16-bit.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac b/crates/pile-audio/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac new file mode 100644 index 0000000..853a37d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/62 - predictor overflow check, 20-bit.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac b/crates/pile-audio/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac new file mode 100644 index 0000000..3a74029 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/63 - predictor overflow check, 24-bit.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/64 - rice partitions with escape code zero.flac b/crates/pile-audio/tests/files/flac_subset/64 - rice partitions with escape code zero.flac new file mode 100644 index 0000000..9aa0961 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_subset/64 - rice partitions with escape code zero.flac differ diff --git a/crates/pile-audio/tests/files/flac_subset/LICENSE.txt b/crates/pile-audio/tests/files/flac_subset/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_subset/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/crates/pile-audio/tests/files/flac_subset/README.md b/crates/pile-audio/tests/files/flac_subset/README.md new file mode 100644 index 0000000..0fa44fe --- /dev/null +++ b/crates/pile-audio/tests/files/flac_subset/README.md @@ -0,0 +1,127 @@ +# Group subset + +The FLAC format specifies a subset of itself to ensure +streamability and limits the decoding requirements for hardware +implementations. The reference FLAC encoder will enforce this +subset unless specifically disabled. + +The files in this group are considered a baseline for general +decoders: these files should be properly decoded or properly +rejected before playback is attempted by any decoder. A +decoder can choose to reject certain files, for example +multichannel files, files with high or unusual samplerates, +files with a high bit depth. Crashing or mangled playback of +these files is probably going to be noticed by unsuspecting +users of a decoder. Read the README.txt in the directory +subset for details on each file. + + +## Files \#1 - \#27 + +The first 10 files tests 44.1kHz, 16-bit audio with various +blocksizes that are within subset. + +Files 11 through 23 tests the ability to decode FLAC files +with various features that are within subset but aren't used +often. + +- File 11 uses the maximal allowed rice partition order (8) +- File 12 uses the maximal allowed qlp precision (15) +- File 13 uses the smallest sane qlp precision (2) +- File 14 uses wasted bits +- File 15 uses only 'verbatim' frames +- File 16 uses rice escape codes and partition order 8 +- File 17 uses all possible fixed orders (especially order 0 which isn't used often) +- File 18 is encoded with precision search, using qlp precisions between 3 and 15 +- File 19 uses a samplerate of 35467Hz +- File 20 uses a samplerate of 39kHz +- File 21 uses a samplerate of 22050Hz +- File 22 has 12 bits per sample +- File 23 has 8 bits per sample + +Files 24 through 27 test the ability to decode a FLAC file with +a variable blocksize. This is a subset feature which is +currently (August 2021) only implemented in the Flake decoder +and its forks/decendants and is not enabled by default. + +With the release of FLAC 1.2.0 in July 2007, the FLAC +specification was augmented to more clearly signal variable +blocksize streams by the use of a special bit in the header. +File 24, 25 and 26 use this format. File 27 follows the old +specification, which is much harder to detect + +- File 24 uses the current format and is created by flake r264 +- File 25 uses the current format and is created by a modified flake r264 creating smaller blocks +- File 26 uses the current format and is created by CUETools.Flake 2.1.6 +- File 27 uses the old format and is created by flake 0.11 + +## Files \#28 - \#37 + +Files 28 through 37 test the ability to decode various +high-resolution FLAC file (96kHz, 24-bit) + +- File 28 uses default settings +- File 29 uses the largest allowed blocksize (16384) +- File 30 uses non-standard blocksize 13456 +- File 31 uses only 32th order predictors +- File 32 uses escape codes and partition order 8 +- File 33 is upsampled to 192kHz +- File 34 is upsampled to 192kHz, uses blocksize 16384, 32th + order predictors only, maximum LPC precision and maximum + partition order +- File 35 uses non-standard samplerate 134560Hz +- File 36 is upsampled to 384kHz +- File 37 has 20 bits per sample + +## Files \#38 - \#44 + +Files 38 through 43 test the ability to decode various +multichannel FLAC files. Each file contains a voice description +of the channels present, so as to see whether the channels are +decoded in the correct lay-out. + +- File 38 is 3.0-channel (left, right, center) +- File 39 is 4.0-channel or quadraphonic +- File 40 is 5.0-channel +- File 41 is 5.1-channel +- File 42 is 6.1-channel +- File 43 is 7.1-channel + +File 44 tests the ability to decode a file with the highest +possible data input per second, staying within subset and using +a standard samplerate. It also only uses 32th order predictors +at the highest possible predictor precision and the largest +blocksize allowed within the FLAC subset making it especially +challenging to decode. + +## Files \#45 - \#59 + +Files 45 through 59 test the ability to handle various streams +with valid but rather unusual or extreme metadata. + +- File 45 has 'unknown number of samples' in STREAMINFO +- File 46 has maximum and minimum framesize set to 'unknown' +- File 47 has only a STREAMINFO block +- File 48 has an extremely large SEEKTABLE +- File 49 has an extremely large PADDING block +- File 50 has an extremely large PICTURE block (JPG of 15.8MB) +- File 51 has an extremely large VORBISCOMMENT block +- File 52 has an extremely large APPLICATION block +- File 53 has a CUESHEET block with absurdly many indexes +- File 54 with the same 20 VORBISCOMMENTs repeated 1000 times +- File 55 has the metadata of track 47-52 combined +- File 56 has a PICTURE with mimetype image/jpeg +- File 57 has a PICTURE with mimetype image/png +- File 58 has a PICTURE with mimetype image/gif +- File 59 has a PICTURE with mimetype image/avif + +## Files \#60 - \#64 + +Miscellaneous, later additions + +- File 60 is mono audio +- File 61, 62 and 63 are signals with rather extreme + characteristics that might trigger overflow if a decoder + uses 32-bit integers to calculate the predictor where 64-bit + integers are appropriate +- File 64 contains rice codes with escape code zero diff --git a/crates/pile-audio/tests/files/flac_uncommon/01 - changing samplerate.flac b/crates/pile-audio/tests/files/flac_uncommon/01 - changing samplerate.flac new file mode 100644 index 0000000..7cd773b Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/01 - changing samplerate.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/02 - increasing number of channels.flac b/crates/pile-audio/tests/files/flac_uncommon/02 - increasing number of channels.flac new file mode 100644 index 0000000..7967165 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/02 - increasing number of channels.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/03 - decreasing number of channels.flac b/crates/pile-audio/tests/files/flac_uncommon/03 - decreasing number of channels.flac new file mode 100644 index 0000000..276a44b Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/03 - decreasing number of channels.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/04 - changing bitdepth.flac b/crates/pile-audio/tests/files/flac_uncommon/04 - changing bitdepth.flac new file mode 100644 index 0000000..c62f76d Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/04 - changing bitdepth.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/05 - 32bps audio.flac b/crates/pile-audio/tests/files/flac_uncommon/05 - 32bps audio.flac new file mode 100644 index 0000000..1e40f60 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/05 - 32bps audio.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/06 - samplerate 768kHz.flac b/crates/pile-audio/tests/files/flac_uncommon/06 - samplerate 768kHz.flac new file mode 100644 index 0000000..e592a56 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/06 - samplerate 768kHz.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/07 - 15 bit per sample.flac b/crates/pile-audio/tests/files/flac_uncommon/07 - 15 bit per sample.flac new file mode 100644 index 0000000..0b3f4e6 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/07 - 15 bit per sample.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/08 - blocksize 65535.flac b/crates/pile-audio/tests/files/flac_uncommon/08 - blocksize 65535.flac new file mode 100644 index 0000000..fd55d36 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/08 - blocksize 65535.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/09 - Rice partition order 15.flac b/crates/pile-audio/tests/files/flac_uncommon/09 - Rice partition order 15.flac new file mode 100644 index 0000000..06c30c6 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/09 - Rice partition order 15.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/10 - file starting at frame header.flac b/crates/pile-audio/tests/files/flac_uncommon/10 - file starting at frame header.flac new file mode 100644 index 0000000..6c87632 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/10 - file starting at frame header.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/11 - file starting with unparsable data.flac b/crates/pile-audio/tests/files/flac_uncommon/11 - file starting with unparsable data.flac new file mode 100644 index 0000000..a8e3580 Binary files /dev/null and b/crates/pile-audio/tests/files/flac_uncommon/11 - file starting with unparsable data.flac differ diff --git a/crates/pile-audio/tests/files/flac_uncommon/LICENSE.txt b/crates/pile-audio/tests/files/flac_uncommon/LICENSE.txt new file mode 100644 index 0000000..0e259d4 --- /dev/null +++ b/crates/pile-audio/tests/files/flac_uncommon/LICENSE.txt @@ -0,0 +1,121 @@ +Creative Commons Legal Code + +CC0 1.0 Universal + + CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE + LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN + ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS + INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES + REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS + PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM + THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED + HEREUNDER. + +Statement of Purpose + +The laws of most jurisdictions throughout the world automatically confer +exclusive Copyright and Related Rights (defined below) upon the creator +and subsequent owner(s) (each and all, an "owner") of an original work of +authorship and/or a database (each, a "Work"). + +Certain owners wish to permanently relinquish those rights to a Work for +the purpose of contributing to a commons of creative, cultural and +scientific works ("Commons") that the public can reliably and without fear +of later claims of infringement build upon, modify, incorporate in other +works, reuse and redistribute as freely as possible in any form whatsoever +and for any purposes, including without limitation commercial purposes. +These owners may contribute to the Commons to promote the ideal of a free +culture and the further production of creative, cultural and scientific +works, or to gain reputation or greater distribution for their Work in +part through the use and efforts of others. + +For these and/or other purposes and motivations, and without any +expectation of additional consideration or compensation, the person +associating CC0 with a Work (the "Affirmer"), to the extent that he or she +is an owner of Copyright and Related Rights in the Work, voluntarily +elects to apply CC0 to the Work and publicly distribute the Work under its +terms, with knowledge of his or her Copyright and Related Rights in the +Work and the meaning and intended legal effect of CC0 on those rights. + +1. Copyright and Related Rights. A Work made available under CC0 may be +protected by copyright and related or neighboring rights ("Copyright and +Related Rights"). Copyright and Related Rights include, but are not +limited to, the following: + + i. the right to reproduce, adapt, distribute, perform, display, + communicate, and translate a Work; + ii. moral rights retained by the original author(s) and/or performer(s); +iii. publicity and privacy rights pertaining to a person's image or + likeness depicted in a Work; + iv. rights protecting against unfair competition in regards to a Work, + subject to the limitations in paragraph 4(a), below; + v. rights protecting the extraction, dissemination, use and reuse of data + in a Work; + vi. database rights (such as those arising under Directive 96/9/EC of the + European Parliament and of the Council of 11 March 1996 on the legal + protection of databases, and under any national implementation + thereof, including any amended or successor version of such + directive); and +vii. other similar, equivalent or corresponding rights throughout the + world based on applicable law or treaty, and any national + implementations thereof. + +2. Waiver. To the greatest extent permitted by, but not in contravention +of, applicable law, Affirmer hereby overtly, fully, permanently, +irrevocably and unconditionally waives, abandons, and surrenders all of +Affirmer's Copyright and Related Rights and associated claims and causes +of action, whether now known or unknown (including existing as well as +future claims and causes of action), in the Work (i) in all territories +worldwide, (ii) for the maximum duration provided by applicable law or +treaty (including future time extensions), (iii) in any current or future +medium and for any number of copies, and (iv) for any purpose whatsoever, +including without limitation commercial, advertising or promotional +purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each +member of the public at large and to the detriment of Affirmer's heirs and +successors, fully intending that such Waiver shall not be subject to +revocation, rescission, cancellation, termination, or any other legal or +equitable action to disrupt the quiet enjoyment of the Work by the public +as contemplated by Affirmer's express Statement of Purpose. + +3. Public License Fallback. Should any part of the Waiver for any reason +be judged legally invalid or ineffective under applicable law, then the +Waiver shall be preserved to the maximum extent permitted taking into +account Affirmer's express Statement of Purpose. In addition, to the +extent the Waiver is so judged Affirmer hereby grants to each affected +person a royalty-free, non transferable, non sublicensable, non exclusive, +irrevocable and unconditional license to exercise Affirmer's Copyright and +Related Rights in the Work (i) in all territories worldwide, (ii) for the +maximum duration provided by applicable law or treaty (including future +time extensions), (iii) in any current or future medium and for any number +of copies, and (iv) for any purpose whatsoever, including without +limitation commercial, advertising or promotional purposes (the +"License"). The License shall be deemed effective as of the date CC0 was +applied by Affirmer to the Work. Should any part of the License for any +reason be judged legally invalid or ineffective under applicable law, such +partial invalidity or ineffectiveness shall not invalidate the remainder +of the License, and in such case Affirmer hereby affirms that he or she +will not (i) exercise any of his or her remaining Copyright and Related +Rights in the Work or (ii) assert any associated claims and causes of +action with respect to the Work, in either case contrary to Affirmer's +express Statement of Purpose. + +4. Limitations and Disclaimers. + + a. No trademark or patent rights held by Affirmer are waived, abandoned, + surrendered, licensed or otherwise affected by this document. + b. Affirmer offers the Work as-is and makes no representations or + warranties of any kind concerning the Work, express, implied, + statutory or otherwise, including without limitation warranties of + title, merchantability, fitness for a particular purpose, non + infringement, or the absence of latent or other defects, accuracy, or + the present or absence of errors, whether or not discoverable, all to + the greatest extent permissible under applicable law. + c. Affirmer disclaims responsibility for clearing rights of other persons + that may apply to the Work or any use thereof, including without + limitation any person's Copyright and Related Rights in the Work. + Further, Affirmer disclaims responsibility for obtaining any necessary + consents, permissions or other rights required for any use of the + Work. + d. Affirmer understands and acknowledges that Creative Commons is not a + party to this document and has no duty or obligation with respect to + this CC0 or use of the Work. diff --git a/crates/pile-audio/tests/files/flac_uncommon/README.md b/crates/pile-audio/tests/files/flac_uncommon/README.md new file mode 100644 index 0000000..97c287d --- /dev/null +++ b/crates/pile-audio/tests/files/flac_uncommon/README.md @@ -0,0 +1,63 @@ +# Group uncommon + +Certain features of the FLAC format are non-subset or otherwise +uncommonly used. Decoders might not be able to playback these +files, but they should detect this inability and preferably +notify the user rather than crash of freeze. Read the +README.txt in the directory uncommon for details on each file. + +Note that despite the features being used are uncommon, these +files are still valid FLAC files. + + +## Manifest + +- File 01 has a sample rate that changes mid-stream. The file + starts with a sample rate of 32kHz and switches to 24kHz, + 16kHz and finally 48kHz. + +- File 02 has a number of channels that increases mid-stream. + The file starts with 1 channel, changes to stereo and ends + with 6 channels. + +- File 03 has a number of channels decreases mid-stream. It + starts with 4 channels, changes to stereo and ends with + 1 channel. + +- File 04 has a bit depth that changes mid-stream. It starts + with a bit depth of 16, changes to 8 and ends with 24. + +- File 05 has a bit depth of 32 bit per sample. + +- File 06 has a sample rate of 768kHz. Because this sample + rate cannot be described in the FLAC frame header, files + with this sample rate are not streamable and not subset. + +- File 07 has a bit depth of 15 bit per sample. Because this + bit depth cannot be described in the FLAC frame header, + files with this bit depth are not streamable and not subset. + +- File 08 has a block size of 65535 samples, which is the + largest possible block size in FLAC. File using such large + blocks are not subset. + +- File 09 has one subframe with a rice partition order of 15, + which is the highest possible partition order. In effect + it means each partition in that subframe contains only a + single sample. Such an extreme might trigger peculiar + behaviour in a decoder. + +- File 10 is a FLAC file that starts directly at a frame + header, which means the fLaC marker is absent and no + metadata is present either. While this is not a recommended + format to store audio data in, it simulates the way a FLAC + stream is received when multicast. Additionally, the frame + numbers of the frames in the stream are rather high which + can happen when streaming uninterrupted for a long time. + +- File 11 is similar in purpose to file 10, but this file has + unparsable data preceding the first FLAC frame header. This + better simulates receiving a multicast FLAC stream, because + the first received packet of a multicast stream might not + align with a frame boundary, as a frame might be split over + several packets.