Initial pile-audio
This commit is contained in:
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;
|
||||
Reference in New Issue
Block a user