pile-audio refactor

This commit is contained in:
2026-02-21 19:19:41 -08:00
parent 5aab61bd1b
commit 6286719e88
136 changed files with 1995 additions and 3390 deletions

View 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)?)
}
})
}
}

View File

@@ -0,0 +1,9 @@
mod block;
pub use block::*;
#[expect(clippy::module_inception)]
mod reader;
pub use reader::*;
#[cfg(test)]
mod tests;

View 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()));
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,397 @@
#![expect(clippy::unwrap_used)]
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 reader = FlacReader::new(file);
let mut out_blocks = Vec::new();
for b in reader {
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);