pile-audio refactor
This commit is contained in:
20
crates/pile-flac/Cargo.toml
Normal file
20
crates/pile-flac/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "pile-flac"
|
||||
version = { workspace = true }
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
mime = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
strum = { workspace = true }
|
||||
smartstring = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
paste = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
79
crates/pile-flac/src/blocks/application.rs
Normal file
79
crates/pile-flac/src/blocks/application.rs
Normal file
@@ -0,0 +1,79 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// 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 {
|
||||
#[expect(clippy::expect_used)]
|
||||
fn get_len(&self) -> u32 {
|
||||
(self.data.len() + 4)
|
||||
.try_into()
|
||||
.expect("application block size does not fit into u32")
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
42
crates/pile-flac/src/blocks/audiodata.rs
Normal file
42
crates/pile-flac/src/blocks/audiodata.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// 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(());
|
||||
}
|
||||
}
|
||||
50
crates/pile-flac/src/blocks/comment.rs
Normal file
50
crates/pile-flac/src/blocks/comment.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError, VorbisComment};
|
||||
|
||||
/// 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(());
|
||||
}
|
||||
}
|
||||
53
crates/pile-flac/src/blocks/cuesheet.rs
Normal file
53
crates/pile-flac/src/blocks/cuesheet.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// 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 {
|
||||
#[expect(clippy::expect_used)]
|
||||
fn get_len(&self) -> u32 {
|
||||
self.data
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("cuesheet size does not fit into u32")
|
||||
}
|
||||
|
||||
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-flac/src/blocks/header.rs
Normal file
87
crates/pile-flac/src/blocks/header.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
//! FLAC metablock headers. See spec.
|
||||
use std::fmt::Debug;
|
||||
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// A type of flac metadata block
|
||||
#[expect(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(());
|
||||
}
|
||||
}
|
||||
52
crates/pile-flac/src/blocks/mod.rs
Normal file
52
crates/pile-flac/src/blocks/mod.rs
Normal file
@@ -0,0 +1,52 @@
|
||||
//! Read and write implementations for all flac block types
|
||||
|
||||
mod header;
|
||||
pub use header::{FlacMetablockHeader, FlacMetablockType};
|
||||
|
||||
mod audiodata;
|
||||
pub use audiodata::FlacAudioFrame;
|
||||
|
||||
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;
|
||||
|
||||
/// 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, crate::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 std::io::Write,
|
||||
) -> Result<(), crate::FlacEncodeError>;
|
||||
}
|
||||
53
crates/pile-flac/src/blocks/padding.rs
Normal file
53
crates/pile-flac/src/blocks/padding.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::{fmt::Debug, io::Read};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// 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 {
|
||||
#[expect(clippy::expect_used)]
|
||||
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()
|
||||
.expect("padding size does not fit into u32"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
213
crates/pile-flac/src/blocks/picture.rs
Normal file
213
crates/pile-flac/src/blocks/picture.rs
Normal file
@@ -0,0 +1,213 @@
|
||||
use mime::Mime;
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
io::{Cursor, Read},
|
||||
};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError, PictureType};
|
||||
|
||||
/// 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 = u32::from_be_bytes(block);
|
||||
let picture_type = PictureType::from_idx(picture_type)
|
||||
.ok_or(FlacDecodeError::InvalidPictureType(picture_type))?;
|
||||
|
||||
// Image format
|
||||
let mime = {
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mime_length = u32::from_be_bytes(block)
|
||||
.try_into()
|
||||
.expect("mime length does not fit into usize");
|
||||
let mut mime = vec![0u8; mime_length];
|
||||
|
||||
d.read_exact(&mut mime)
|
||||
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
|
||||
|
||||
String::from_utf8(mime)
|
||||
.ok()
|
||||
.and_then(|x| x.parse().ok())
|
||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM)
|
||||
};
|
||||
|
||||
// Image description
|
||||
let description = {
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let desc_length = u32::from_be_bytes(block)
|
||||
.try_into()
|
||||
.expect("description length does not fit into usize");
|
||||
let mut desc = vec![0u8; desc_length];
|
||||
|
||||
d.read_exact(&mut desc)
|
||||
.map_err(|_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 = {
|
||||
d.read_exact(&mut block)
|
||||
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let data_length = u32::from_be_bytes(block)
|
||||
.try_into()
|
||||
.expect("image data length does not fit into usize");
|
||||
let mut img_data = vec![0u8; data_length];
|
||||
|
||||
d.read_exact(&mut img_data)
|
||||
.map_err(|_err| FlacDecodeError::MalformedBlock)?;
|
||||
|
||||
img_data
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
picture_type,
|
||||
mime,
|
||||
description,
|
||||
width,
|
||||
height,
|
||||
bit_depth: depth,
|
||||
color_count,
|
||||
img_data,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl FlacMetablockEncode for FlacPictureBlock {
|
||||
#[expect(clippy::expect_used)]
|
||||
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()
|
||||
.expect("picture block size does not fit into u32")
|
||||
}
|
||||
|
||||
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())?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
{
|
||||
let mime = self.mime.to_string();
|
||||
target.write_all(
|
||||
&u32::try_from(mime.len())
|
||||
.expect("mime length does not fit into u32")
|
||||
.to_be_bytes(),
|
||||
)?;
|
||||
target.write_all(self.mime.to_string().as_bytes())?;
|
||||
drop(mime);
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(self.description.len())
|
||||
.expect("description length does not fit into u32")
|
||||
.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())
|
||||
.expect("image data length does not fit into u32")
|
||||
.to_be_bytes(),
|
||||
)?;
|
||||
}
|
||||
target.write_all(&self.img_data)?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
53
crates/pile-flac/src/blocks/seektable.rs
Normal file
53
crates/pile-flac/src/blocks/seektable.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::fmt::Debug;
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// 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 {
|
||||
#[expect(clippy::expect_used)]
|
||||
fn get_len(&self) -> u32 {
|
||||
self.data
|
||||
.len()
|
||||
.try_into()
|
||||
.expect("seektable size does not fit into u32")
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
}
|
||||
217
crates/pile-flac/src/blocks/streaminfo.rs
Normal file
217
crates/pile-flac/src/blocks/streaminfo.rs
Normal file
@@ -0,0 +1,217 @@
|
||||
use std::io::{Cursor, Read};
|
||||
|
||||
use super::{FlacMetablockDecode, FlacMetablockEncode, FlacMetablockHeader, FlacMetablockType};
|
||||
use crate::{FlacDecodeError, FlacEncodeError};
|
||||
|
||||
/// 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(());
|
||||
}
|
||||
}
|
||||
54
crates/pile-flac/src/errors.rs
Normal file
54
crates/pile-flac/src/errors.rs
Normal file
@@ -0,0 +1,54 @@
|
||||
//! FLAC errors
|
||||
use std::string::FromUtf8Error;
|
||||
use thiserror::Error;
|
||||
|
||||
#[expect(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("i/o error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
/// We tried to decode a string, but found invalid UTF-8
|
||||
#[error("could not decode string")]
|
||||
FailedStringDecode(#[from] FromUtf8Error),
|
||||
|
||||
/// We tried to read a block, but it was out of spec.
|
||||
#[error("malformed flac block")]
|
||||
MalformedBlock,
|
||||
|
||||
/// We could not parse a vorbis comment string
|
||||
#[error("malformed vorbis comment string: {0:?}")]
|
||||
MalformedCommentString(String),
|
||||
|
||||
/// We could not parse a vorbis comment string
|
||||
#[error("malformed vorbis picture")]
|
||||
MalformedPicture(base64::DecodeError),
|
||||
|
||||
/// We didn't find frame sync bytes where we expected them
|
||||
#[error("bad frame sync bytes")]
|
||||
BadSyncBytes,
|
||||
|
||||
#[error("invalid picture type {0}")]
|
||||
InvalidPictureType(u32),
|
||||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FlacEncodeError {
|
||||
/// We encountered an i/o error while processing
|
||||
#[error("i/o error")]
|
||||
IoError(#[from] std::io::Error),
|
||||
}
|
||||
19
crates/pile-flac/src/lib.rs
Normal file
19
crates/pile-flac/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
pub mod blocks;
|
||||
|
||||
mod tagtype;
|
||||
pub use tagtype::*;
|
||||
|
||||
mod picturetype;
|
||||
pub use picturetype::*;
|
||||
|
||||
mod errors;
|
||||
pub use errors::*;
|
||||
|
||||
mod reader;
|
||||
pub use reader::*;
|
||||
|
||||
mod vorbiscomment;
|
||||
pub use vorbiscomment::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
86
crates/pile-flac/src/picturetype.rs
Normal file
86
crates/pile-flac/src/picturetype.rs
Normal file
@@ -0,0 +1,86 @@
|
||||
//! An audio picture type, according to the ID3v2 APIC frame
|
||||
|
||||
/// A picture type according to the ID3v2 APIC frame
|
||||
#[expect(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) -> Option<Self> {
|
||||
Some(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 None,
|
||||
})
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
63
crates/pile-flac/src/reader/block.rs
Normal file
63
crates/pile-flac/src/reader/block.rs
Normal file
@@ -0,0 +1,63 @@
|
||||
use std::io::Write;
|
||||
|
||||
use crate::{
|
||||
FlacDecodeError, FlacEncodeError,
|
||||
blocks::{
|
||||
FlacApplicationBlock, FlacAudioFrame, FlacCommentBlock, FlacCuesheetBlock,
|
||||
FlacMetablockDecode, FlacMetablockEncode, FlacMetablockType, FlacPaddingBlock,
|
||||
FlacPictureBlock, FlacSeektableBlock, FlacStreaminfoBlock,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
#[expect(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)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
9
crates/pile-flac/src/reader/mod.rs
Normal file
9
crates/pile-flac/src/reader/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
mod block;
|
||||
pub use block::*;
|
||||
|
||||
#[expect(clippy::module_inception)]
|
||||
mod reader;
|
||||
pub use reader::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
184
crates/pile-flac/src/reader/reader.rs
Normal file
184
crates/pile-flac/src/reader/reader.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
use crate::{
|
||||
FlacBlock, FlacDecodeError,
|
||||
blocks::{FlacAudioFrame, FlacMetablockHeader, FlacMetablockType},
|
||||
};
|
||||
|
||||
// TODO: quickly skip blocks we do not need
|
||||
|
||||
/// The next block we expect to read
|
||||
enum ReaderState {
|
||||
MagicBits,
|
||||
MetablockHeader { is_first: bool },
|
||||
MetaBlock { header: FlacMetablockHeader },
|
||||
AudioData,
|
||||
Done,
|
||||
}
|
||||
|
||||
pub struct FlacReader<R: Read + Seek> {
|
||||
inner: R,
|
||||
state: ReaderState,
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> FlacReader<R> {
|
||||
const MIN_AUDIO_FRAME_LEN: usize = 5000;
|
||||
|
||||
pub fn new(inner: R) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
state: ReaderState::MagicBits,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read + Seek> Iterator for FlacReader<R> {
|
||||
type Item = Result<FlacBlock, FlacDecodeError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
loop {
|
||||
match &mut self.state {
|
||||
ReaderState::Done => return None,
|
||||
|
||||
ReaderState::MagicBits => {
|
||||
let mut data = [0u8; 4];
|
||||
if let Err(e) = self.inner.read_exact(&mut data[..4]) {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
|
||||
if data != [0x66, 0x4C, 0x61, 0x43] {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(FlacDecodeError::BadMagicBytes));
|
||||
}
|
||||
|
||||
self.state = ReaderState::MetablockHeader { is_first: true };
|
||||
}
|
||||
|
||||
ReaderState::MetablockHeader { is_first } => {
|
||||
let mut data = [0u8; 4];
|
||||
if let Err(e) = self.inner.read_exact(&mut data[..]) {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
|
||||
let header = match FlacMetablockHeader::decode(&data) {
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e));
|
||||
}
|
||||
};
|
||||
|
||||
if *is_first && !matches!(header.block_type, FlacMetablockType::Streaminfo) {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(FlacDecodeError::BadFirstBlock));
|
||||
}
|
||||
|
||||
self.state = ReaderState::MetaBlock { header };
|
||||
}
|
||||
|
||||
ReaderState::MetaBlock { header } => {
|
||||
let mut data = vec![0u8; header.length as usize];
|
||||
if let Err(e) = self.inner.read_exact(&mut data) {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
|
||||
let block = match FlacBlock::decode(header.block_type, &data) {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e));
|
||||
}
|
||||
};
|
||||
|
||||
if header.is_last {
|
||||
self.state = ReaderState::AudioData;
|
||||
} else {
|
||||
self.state = ReaderState::MetablockHeader { is_first: false };
|
||||
}
|
||||
|
||||
return Some(Ok(block));
|
||||
}
|
||||
|
||||
ReaderState::AudioData => {
|
||||
let mut data = Vec::new();
|
||||
loop {
|
||||
let mut byte = [0u8; 1];
|
||||
match self.inner.read_exact(&mut byte) {
|
||||
Ok(_) => {
|
||||
data.push(byte[0]);
|
||||
|
||||
if data.len() >= Self::MIN_AUDIO_FRAME_LEN + 2 {
|
||||
let len = data.len();
|
||||
if data[len - 2] == 0b1111_1111
|
||||
&& data[len - 1] & 0b1111_1100 == 0b1111_1000
|
||||
{
|
||||
let frame_data = data[..len - 2].to_vec();
|
||||
|
||||
if frame_data.len() < 2
|
||||
|| frame_data[0] != 0b1111_1111 || frame_data[1]
|
||||
& 0b1111_1100
|
||||
!= 0b1111_1000
|
||||
{
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(FlacDecodeError::BadSyncBytes));
|
||||
}
|
||||
|
||||
let audio_frame = match FlacAudioFrame::decode(&frame_data)
|
||||
{
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Seek back 2 bytes so the next frame starts with the sync bytes
|
||||
if let Err(e) = self.inner.seek(SeekFrom::Current(-2)) {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
|
||||
self.state = ReaderState::AudioData;
|
||||
|
||||
return Some(Ok(FlacBlock::AudioFrame(audio_frame)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
if data.len() > 2 {
|
||||
if data[0] != 0b1111_1111
|
||||
|| data[1] & 0b1111_1100 != 0b1111_1000
|
||||
{
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(FlacDecodeError::BadSyncBytes));
|
||||
}
|
||||
|
||||
let audio_frame = match FlacAudioFrame::decode(&data) {
|
||||
Ok(f) => f,
|
||||
Err(e) => {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e));
|
||||
}
|
||||
};
|
||||
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Ok(FlacBlock::AudioFrame(audio_frame)));
|
||||
} else {
|
||||
self.state = ReaderState::Done;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
self.state = ReaderState::Done;
|
||||
return Some(Err(e.into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
393
crates/pile-flac/src/reader/tests.rs
Normal file
393
crates/pile-flac/src/reader/tests.rs
Normal file
@@ -0,0 +1,393 @@
|
||||
use itertools::Itertools;
|
||||
use paste::paste;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs::File, io::Write, str::FromStr};
|
||||
|
||||
use crate::{
|
||||
FlacDecodeError, TagType,
|
||||
tests::{FlacBlockOutput, FlacTestCase, VorbisCommentTestValue},
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn read_file(test_case: &FlacTestCase) -> Result<Vec<FlacBlock>, FlacDecodeError> {
|
||||
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 file = File::open(test_case.get_path()).unwrap();
|
||||
|
||||
let mut reader = FlacReader::new(file);
|
||||
let mut out_blocks = Vec::new();
|
||||
|
||||
while let Some(b) = reader.next() {
|
||||
out_blocks.push(b?)
|
||||
}
|
||||
|
||||
return Ok(out_blocks);
|
||||
}
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
fn test_identical(test_case: &FlacTestCase) -> Result<(), FlacDecodeError> {
|
||||
let out_blocks = read_file(test_case)?;
|
||||
|
||||
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) -> Result<(), FlacDecodeError> {
|
||||
let out_blocks = read_file(test_case)?;
|
||||
|
||||
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_val};", got_tag.to_vorbis_string()).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 = crate::tests::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,
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_blockread(test_case);
|
||||
match e {
|
||||
Err(e) => assert!(check_error(&e), "Unexpected error {e:?}"),
|
||||
_ => panic!("Unexpected error {e:?}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn [<identical_small_ $test_name>]() {
|
||||
let manifest = crate::tests::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,
|
||||
).unwrap()
|
||||
}
|
||||
},
|
||||
|
||||
FlacTestCase::Error { check_error, .. } => {
|
||||
let e = test_identical(test_case);
|
||||
match e {
|
||||
Err(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);
|
||||
78
crates/pile-flac/src/tagtype.rs
Normal file
78
crates/pile-flac/src/tagtype.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
//! 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)]
|
||||
#[strum(ascii_case_insensitive)]
|
||||
pub enum TagType {
|
||||
/// A tag we didn't recognize
|
||||
#[strum(default)]
|
||||
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,
|
||||
}
|
||||
|
||||
impl TagType {
|
||||
pub fn to_vorbis_string(&self) -> String {
|
||||
match self {
|
||||
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()
|
||||
}
|
||||
}
|
||||
919
crates/pile-flac/src/tests.rs
Normal file
919
crates/pile-flac/src/tests.rs
Normal file
@@ -0,0 +1,919 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use itertools::Itertools;
|
||||
use mime::Mime;
|
||||
|
||||
use crate::PictureType;
|
||||
|
||||
use super::errors::FlacDecodeError;
|
||||
|
||||
/// 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>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[expect(dead_code)]
|
||||
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
|
||||
#[expect(clippy::unwrap_used)]
|
||||
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::IoError(_)),
|
||||
// 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: "0371b6f158411f35121f1d62ccbc18c90c9b1b0263e51bfc1b8fc942892eaf12",
|
||||
},
|
||||
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: "8d8f21954b4aaee2c7ec92389125a9b28b7de5a8153c62abdd80330f445214df",
|
||||
},
|
||||
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::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());
|
||||
}
|
||||
194
crates/pile-flac/src/vorbiscomment.rs
Normal file
194
crates/pile-flac/src/vorbiscomment.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
//! Decode and write Vorbis comment blocks
|
||||
|
||||
use base64::Engine;
|
||||
use smartstring::{LazyCompact, SmartString};
|
||||
use std::{
|
||||
io::{Cursor, Read, Write},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use super::tagtype::TagType;
|
||||
use crate::{
|
||||
FlacDecodeError, FlacEncodeError,
|
||||
blocks::{FlacMetablockDecode, FlacMetablockEncode, FlacPictureBlock},
|
||||
};
|
||||
|
||||
/// 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, FlacDecodeError> {
|
||||
let mut d = Cursor::new(data);
|
||||
|
||||
// This is re-used whenever we need to read four bytes
|
||||
let mut block = [0u8; 4];
|
||||
|
||||
let vendor = {
|
||||
d.read_exact(&mut block)?;
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("vendor length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)?;
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
|
||||
d.read_exact(&mut block)?;
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let n_comments: usize = u32::from_le_bytes(block)
|
||||
.try_into()
|
||||
.expect("comment count does not fit into usize");
|
||||
|
||||
let mut comments = Vec::new();
|
||||
let mut pictures = Vec::new();
|
||||
for _ in 0..n_comments {
|
||||
let comment = {
|
||||
d.read_exact(&mut block)?;
|
||||
|
||||
let length = u32::from_le_bytes(block);
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
let mut text = vec![
|
||||
0u8;
|
||||
length
|
||||
.try_into()
|
||||
.expect("comment length does not fit into usize")
|
||||
];
|
||||
|
||||
d.read_exact(&mut text)?;
|
||||
|
||||
String::from_utf8(text)?
|
||||
};
|
||||
let (var, val) = comment
|
||||
.split_once('=')
|
||||
.ok_or(FlacDecodeError::MalformedCommentString(comment.clone()))?;
|
||||
|
||||
if !val.is_empty() {
|
||||
if var.to_uppercase() == "METADATA_BLOCK_PICTURE" {
|
||||
pictures.push(FlacPictureBlock::decode(
|
||||
&base64::prelude::BASE64_STANDARD
|
||||
.decode(val)
|
||||
.map_err(FlacDecodeError::MalformedPicture)?,
|
||||
)?);
|
||||
} else {
|
||||
// Make sure empty strings are saved as "None"
|
||||
comments.push((
|
||||
TagType::from_str(var).unwrap_or(TagType::Other(var.into())),
|
||||
val.into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
vendor: vendor.into(),
|
||||
comments,
|
||||
pictures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl VorbisComment {
|
||||
/// Get the number of bytes that `encode()` will write.
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn get_len(&self) -> u32 {
|
||||
let mut sum: u32 = 0;
|
||||
sum += u32::try_from(self.vendor.len()).expect("vendor length does not fit into u32") + 4;
|
||||
sum += 4;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = tagtype.to_vorbis_string();
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
sum +=
|
||||
4 + u32::try_from(str.len()).expect("comment string length does not fit into u32");
|
||||
}
|
||||
|
||||
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
|
||||
#[expect(clippy::expect_used)]
|
||||
pub fn encode(&self, target: &mut impl Write) -> Result<(), FlacEncodeError> {
|
||||
target.write_all(
|
||||
&u32::try_from(self.vendor.len())
|
||||
.expect("vendor length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(self.vendor.as_bytes())?;
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(self.comments.len() + self.pictures.len())
|
||||
.expect("total comment count does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
|
||||
for (tagtype, value) in &self.comments {
|
||||
let tagtype_str = tagtype.to_vorbis_string();
|
||||
let str = format!("{tagtype_str}={value}");
|
||||
target.write_all(
|
||||
&u32::try_from(str.len())
|
||||
.expect("comment string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(str.as_bytes())?;
|
||||
}
|
||||
|
||||
for p in &self.pictures {
|
||||
let mut pic_data = Vec::new();
|
||||
p.encode(false, false, &mut pic_data)?;
|
||||
|
||||
let pic_string = format!(
|
||||
"METADATA_BLOCK_PICTURE={}",
|
||||
&base64::prelude::BASE64_STANDARD.encode(&pic_data)
|
||||
);
|
||||
|
||||
target.write_all(
|
||||
&u32::try_from(pic_string.len())
|
||||
.expect("picture string length does not fit into u32")
|
||||
.to_le_bytes(),
|
||||
)?;
|
||||
target.write_all(pic_string.as_bytes())?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
13
crates/pile-flac/tests/files/README.md
Normal file
13
crates/pile-flac/tests/files/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Audio files for tests
|
||||
|
||||
## Sources
|
||||
- `./flac_subset`: Files from the "subset" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main)
|
||||
- `./flac_faulty`: Files from the "faulty" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main)
|
||||
- `./flac_uncommon`: Files from the "uncommon" group of [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main)
|
||||
- `./flac_custom`: Custom files based on [the flac test kit](https://github.com/ietf-wg-cellar/flac-test-files/tree/main)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
BIN
crates/pile-flac/tests/files/flac_custom/01 - many images.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_custom/01 - many images.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
121
crates/pile-flac/tests/files/flac_custom/LICENSE.txt
Normal file
121
crates/pile-flac/tests/files/flac_custom/LICENSE.txt
Normal file
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
17
crates/pile-flac/tests/files/flac_custom/README.md
Normal file
17
crates/pile-flac/tests/files/flac_custom/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# Custom FLAC test files
|
||||
|
||||
These are flac files created specifically for Copper, and test cases that the flac test toolkit doesn't cover.
|
||||
Most of these are modified copies of files in `flac_subset`, `flac_faulty`, or `flac_uncommon`
|
||||
|
||||
|
||||
## Manifest
|
||||
|
||||
- `01 - many images.flac`: This is `flac_subset/50` with additional images from `56`, `57`, `58`, and `59`, in that order.
|
||||
- Image 0: from file `50`, type is `3`, description is empty.
|
||||
- Image 1: from file `56`, type is `17`, description is `lorem`.
|
||||
- Image 2: from file `57`, type is `2`, description is `ipsum`.
|
||||
- Image 3: from file `58`, type is `12`, description is `dolor`.
|
||||
- Image 4: from file `59`, type is `4`, description is `est`.
|
||||
- Image `57` is also stored in the vorbis comment as a `METADATA_BLOCK_PICTURE`.
|
||||
- `02 - picture in vorbis comment.flac`: This is `flac_subset/57`, but with the image stored inside a vorbis `METADATA_BLOCK_PICTURE` comment instead of a proper flac picture metablock.
|
||||
- `03 - faulty picture in vorbis comment.flac`: This is `02`, but with a corrupt picture.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_faulty/09 - blocksize 1.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_faulty/09 - blocksize 1.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
121
crates/pile-flac/tests/files/flac_faulty/LICENSE.txt
Normal file
121
crates/pile-flac/tests/files/flac_faulty/LICENSE.txt
Normal file
@@ -0,0 +1,121 @@
|
||||
Creative Commons Legal Code
|
||||
|
||||
CC0 1.0 Universal
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
62
crates/pile-flac/tests/files/flac_faulty/README.md
Normal file
62
crates/pile-flac/tests/files/flac_faulty/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Group faulty
|
||||
|
||||
This group contains files with invalid data and corrupted files
|
||||
that might trigger crashes in a decoder. A decoder should not
|
||||
crash or freeze reading these files. Read the README.txt in
|
||||
the directory faulty for details on each file.
|
||||
|
||||
|
||||
## Manifest
|
||||
|
||||
- File 01 has a streaminfo metadata block that lists the wrong
|
||||
maximum blocksize for the file. The file has a fixed
|
||||
blocksize of 16384 samples, but the streaminfo metadata block
|
||||
says the maximum blocksize is 4096. When a decoder
|
||||
initializes buffers for a blocksize of 4096 and tries to
|
||||
decode a block with 16384 samples, it might overrun a buffer
|
||||
|
||||
- File 02 has a streaminfo metadata block that lists the wrong
|
||||
maximum framesize for the file. The file has an actual
|
||||
maximum framesize of 8846 byte, but the streaminfo metadata
|
||||
block says the maximum framesize is 654 byte. When a decoder
|
||||
initializes buffers for a frames of at most 654 byte and
|
||||
tries to read a frame of 8846 byte, it might overrun a buffer
|
||||
|
||||
- File 03 has a streaminfo metadata block that lists the wrong
|
||||
bit depth for the file. It says the bit depth is 24, but the
|
||||
actual bit depth of all frames is 16.
|
||||
|
||||
- File 04 has a streaminfo metadata block that lists the wrong
|
||||
number of channels for the file. It says the number of
|
||||
channels is 5, but the actual number of channels is 1.
|
||||
|
||||
- File 05 has a streaminfo metadata block that lists the wrong
|
||||
total number of samples. It says the number of samples is
|
||||
39842, while the actual total number of samples is 109487
|
||||
|
||||
- File 06 doesn't have a streaminfo metadata block, despite
|
||||
having other metadata blocks, unlike the files 10 and 11 in
|
||||
the 'uncommon' set of files, which start directly at a frame
|
||||
header. It does have two other metadata blocks, a vorbis
|
||||
comment block and a padding block
|
||||
|
||||
- File 07 has a streaminfo metadata block that is not the first
|
||||
metadata block of the file. It is being preceded by two other
|
||||
metadata blocks, a vorbis comment block and a padding block
|
||||
|
||||
- File 08 has a blocksize of 65536, which is representable by
|
||||
the frame header but not by the streaminfo metadata block,
|
||||
and is thus invalid
|
||||
|
||||
- File 09 has a blocksize of 1, which is not allowed for all
|
||||
but the last frame of a file
|
||||
|
||||
- File 10 has a vorbis comment metadata block with invalid
|
||||
contents. The block has a correct overall length and has a
|
||||
single tag, but it states there are 10 tags, which means a
|
||||
parser could overread the block if it does not check for
|
||||
validity.
|
||||
|
||||
- File 11 has an incorrect metadata block length, which leads
|
||||
to the parser searching for the next metadata block reading
|
||||
garbage.
|
||||
Binary file not shown.
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/03 - blocksize 16.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/03 - blocksize 16.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/04 - blocksize 192.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/04 - blocksize 192.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/05 - blocksize 254.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/05 - blocksize 254.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/06 - blocksize 512.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/06 - blocksize 512.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/07 - blocksize 725.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/07 - blocksize 725.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/14 - wasted bits.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/14 - wasted bits.flac
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/56 - JPG PICTURE.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/56 - JPG PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/57 - PNG PICTURE.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/57 - PNG PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/58 - GIF PICTURE.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/58 - GIF PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/59 - AVIF PICTURE.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/59 - AVIF PICTURE.flac
Normal file
Binary file not shown.
BIN
crates/pile-flac/tests/files/flac_subset/60 - mono audio.flac
Normal file
BIN
crates/pile-flac/tests/files/flac_subset/60 - mono audio.flac
Normal file
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user