Initial pile-audio
This commit is contained in:
21
crates/pile-audio/Cargo.toml
Normal file
21
crates/pile-audio/Cargo.toml
Normal file
@@ -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 }
|
||||||
5
crates/pile-audio/src/common/mod.rs
Normal file
5
crates/pile-audio/src/common/mod.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
//! Components shared between many different formats
|
||||||
|
|
||||||
|
pub mod picturetype;
|
||||||
|
pub mod tagtype;
|
||||||
|
pub mod vorbiscomment;
|
||||||
102
crates/pile-audio/src/common/picturetype.rs
Normal file
102
crates/pile-audio/src/common/picturetype.rs
Normal file
@@ -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<Self, PictureTypeError> {
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
40
crates/pile-audio/src/common/tagtype.rs
Normal file
40
crates/pile-audio/src/common/tagtype.rs
Normal file
@@ -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<LazyCompact>),
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
327
crates/pile-audio/src/common/vorbiscomment.rs
Normal file
327
crates/pile-audio/src/common/vorbiscomment.rs
Normal file
@@ -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<std::io::Error> for VorbisCommentDecodeError {
|
||||||
|
fn from(value: std::io::Error) -> Self {
|
||||||
|
Self::IoError(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FromUtf8Error> 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<std::io::Error> 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<LazyCompact>,
|
||||||
|
|
||||||
|
/// List of (tag, value)
|
||||||
|
/// Repeated tags are allowed!
|
||||||
|
pub comments: Vec<(TagType, SmartString<LazyCompact>)>,
|
||||||
|
|
||||||
|
/// A list of pictures found in this comment
|
||||||
|
pub pictures: Vec<FlacPictureBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VorbisComment {
|
||||||
|
/// Try to decode the given data as a vorbis comment block
|
||||||
|
pub fn decode(data: &[u8]) -> Result<Self, VorbisCommentDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
840
crates/pile-audio/src/flac/blockread.rs
Normal file
840
crates/pile-audio/src/flac/blockread.rs
Normal file
@@ -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<u8>,
|
||||||
|
},
|
||||||
|
AudioData {
|
||||||
|
data: Vec<u8>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<Self, FlacDecodeError> {
|
||||||
|
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<FlacBlockType>,
|
||||||
|
|
||||||
|
// Blocks we pick go here
|
||||||
|
output_blocks: VecDeque<FlacBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FlacBlockReader {
|
||||||
|
/// Pop the next block we've read, if any.
|
||||||
|
pub fn pop_block(&mut self) -> Option<FlacBlock> {
|
||||||
|
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<Range<usize>>,
|
||||||
|
selector: FlacBlockSelector,
|
||||||
|
) -> Result<Vec<FlacBlock>, 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<Range<usize>>,
|
||||||
|
) -> 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<Range<usize>>,
|
||||||
|
) -> 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 [<blockread_small_ $test_name>]() {
|
||||||
|
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 [<identical_small_ $test_name>]() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
76
crates/pile-audio/src/flac/blocks/application.rs
Normal file
76
crates/pile-audio/src/flac/blocks/application.rs
Normal file
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
43
crates/pile-audio/src/flac/blocks/audiodata.rs
Normal file
43
crates/pile-audio/src/flac/blocks/audiodata.rs
Normal file
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
54
crates/pile-audio/src/flac/blocks/comment.rs
Normal file
54
crates/pile-audio/src/flac/blocks/comment.rs
Normal file
@@ -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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
50
crates/pile-audio/src/flac/blocks/cuesheet.rs
Normal file
50
crates/pile-audio/src/flac/blocks/cuesheet.rs
Normal file
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
87
crates/pile-audio/src/flac/blocks/header.rs
Normal file
87
crates/pile-audio/src/flac/blocks/header.rs
Normal file
@@ -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<Self, FlacDecodeError> {
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
58
crates/pile-audio/src/flac/blocks/mod.rs
Normal file
58
crates/pile-audio/src/flac/blocks/mod.rs
Normal file
@@ -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<Self, FlacDecodeError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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>;
|
||||||
|
}
|
||||||
50
crates/pile-audio/src/flac/blocks/padding.rs
Normal file
50
crates/pile-audio/src/flac/blocks/padding.rs
Normal file
@@ -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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
197
crates/pile-audio/src/flac/blocks/picture.rs
Normal file
197
crates/pile-audio/src/flac/blocks/picture.rs
Normal file
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
50
crates/pile-audio/src/flac/blocks/seektable.rs
Normal file
50
crates/pile-audio/src/flac/blocks/seektable.rs
Normal file
@@ -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<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
218
crates/pile-audio/src/flac/blocks/streaminfo.rs
Normal file
218
crates/pile-audio/src/flac/blocks/streaminfo.rs
Normal file
@@ -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<Self, FlacDecodeError> {
|
||||||
|
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(());
|
||||||
|
}
|
||||||
|
}
|
||||||
68
crates/pile-audio/src/flac/errors.rs
Normal file
68
crates/pile-audio/src/flac/errors.rs
Normal file
@@ -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<VorbisCommentEncodeError> for FlacEncodeError {
|
||||||
|
fn from(value: VorbisCommentEncodeError) -> Self {
|
||||||
|
match value {
|
||||||
|
VorbisCommentEncodeError::IoError(e) => e.into(),
|
||||||
|
VorbisCommentEncodeError::PictureEncodeError => Self::VorbisPictureEncodeError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
942
crates/pile-audio/src/flac/mod.rs
Normal file
942
crates/pile-audio/src/flac/mod.rs
Normal file
@@ -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=<n> \
|
||||||
|
// --data-format=binary-headerless \
|
||||||
|
// <file> \
|
||||||
|
// | 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<FlacBlockOutput>,
|
||||||
|
|
||||||
|
/// 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 \
|
||||||
|
/// <file>
|
||||||
|
/// ```
|
||||||
|
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<Vec<FlacBlockOutput>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Vec<FlacBlockOutput>> {
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
208
crates/pile-audio/src/flac/proc/metastrip.rs
Normal file
208
crates/pile-audio/src/flac/proc/metastrip.rs
Normal file
@@ -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<FlacBlock>,
|
||||||
|
|
||||||
|
/// 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<std::ops::Range<usize>>,
|
||||||
|
) -> 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 [<strip_ $test_name>]() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
5
crates/pile-audio/src/flac/proc/mod.rs
Normal file
5
crates/pile-audio/src/flac/proc/mod.rs
Normal file
@@ -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;
|
||||||
228
crates/pile-audio/src/flac/proc/pictures.rs
Normal file
228
crates/pile-audio/src/flac/proc/pictures.rs
Normal file
@@ -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<FlacPictureBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<FlacPictureBlock> {
|
||||||
|
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<std::ops::Range<usize>>,
|
||||||
|
) -> 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 [<pictures_ $test_name>]() {
|
||||||
|
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);
|
||||||
|
}
|
||||||
3
crates/pile-audio/src/lib.rs
Normal file
3
crates/pile-audio/src/lib.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
//! Read and write audio file metadata.
|
||||||
|
pub mod common;
|
||||||
|
pub mod flac;
|
||||||
13
crates/pile-audio/tests/files/README.md
Normal file
13
crates/pile-audio/tests/files/README.md
Normal file
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
BIN
crates/pile-audio/tests/files/flac_custom/01 - many images.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_custom/01 - many images.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
121
crates/pile-audio/tests/files/flac_custom/LICENSE.txt
Normal file
121
crates/pile-audio/tests/files/flac_custom/LICENSE.txt
Normal file
@@ -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.
|
||||||
17
crates/pile-audio/tests/files/flac_custom/README.md
Normal file
17
crates/pile-audio/tests/files/flac_custom/README.md
Normal file
@@ -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.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_faulty/09 - blocksize 1.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
121
crates/pile-audio/tests/files/flac_faulty/LICENSE.txt
Normal file
121
crates/pile-audio/tests/files/flac_faulty/LICENSE.txt
Normal file
@@ -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.
|
||||||
62
crates/pile-audio/tests/files/flac_faulty/README.md
Normal file
62
crates/pile-audio/tests/files/flac_faulty/README.md
Normal file
@@ -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.
|
||||||
Binary file not shown.
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/03 - blocksize 16.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/14 - wasted bits.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/56 - JPG PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/57 - PNG PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/58 - GIF PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac
Normal file
BIN
crates/pile-audio/tests/files/flac_subset/59 - AVIF PICTURE.flac
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user