use core::fmt::{Display, Formatter, Result as FmtResult};
use std::collections::HashMap;
use crate::error::{AudioIOError, AudioIOResult, ErrorPosition};
use crate::flac::constants::{MD5_SIZE, STREAMINFO_SIZE};
use crate::flac::error::FlacError;
use crate::traits::AudioInfoMarker;
use crate::types::ValidatedSampleType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum MetadataBlockType {
StreamInfo = 0,
Padding = 1,
Application = 2,
SeekTable = 3,
VorbisComment = 4,
CueSheet = 5,
Picture = 6,
Reserved(u8),
}
impl MetadataBlockType {
pub const fn from_byte(value: u8) -> Self {
match value {
0 => MetadataBlockType::StreamInfo,
1 => MetadataBlockType::Padding,
2 => MetadataBlockType::Application,
3 => MetadataBlockType::SeekTable,
4 => MetadataBlockType::VorbisComment,
5 => MetadataBlockType::CueSheet,
6 => MetadataBlockType::Picture,
n => MetadataBlockType::Reserved(n),
}
}
pub const fn as_byte(self) -> u8 {
match self {
MetadataBlockType::StreamInfo => 0,
MetadataBlockType::Padding => 1,
MetadataBlockType::Application => 2,
MetadataBlockType::SeekTable => 3,
MetadataBlockType::VorbisComment => 4,
MetadataBlockType::CueSheet => 5,
MetadataBlockType::Picture => 6,
MetadataBlockType::Reserved(n) => n,
}
}
}
impl Display for MetadataBlockType {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match self {
MetadataBlockType::StreamInfo => write!(f, "STREAMINFO"),
MetadataBlockType::Padding => write!(f, "PADDING"),
MetadataBlockType::Application => write!(f, "APPLICATION"),
MetadataBlockType::SeekTable => write!(f, "SEEKTABLE"),
MetadataBlockType::VorbisComment => write!(f, "VORBIS_COMMENT"),
MetadataBlockType::CueSheet => write!(f, "CUESHEET"),
MetadataBlockType::Picture => write!(f, "PICTURE"),
MetadataBlockType::Reserved(n) => write!(f, "RESERVED({})", n),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct MetadataBlockHeader {
pub is_last: bool,
pub block_type: MetadataBlockType,
pub length: u32,
}
impl MetadataBlockHeader {
pub fn from_bytes(bytes: &[u8; 4]) -> Self {
let is_last = (bytes[0] & 0x80) != 0;
let block_type = MetadataBlockType::from_byte(bytes[0] & 0x7F);
let length = u32::from_be_bytes([0, bytes[1], bytes[2], bytes[3]]);
MetadataBlockHeader {
is_last,
block_type,
length,
}
}
pub fn to_bytes(&self) -> [u8; 4] {
let type_byte = self.block_type.as_byte() | if self.is_last { 0x80 } else { 0 };
let len_bytes = self.length.to_be_bytes();
[type_byte, len_bytes[1], len_bytes[2], len_bytes[3]]
}
pub const fn total_size(&self) -> usize {
4 + self.length as usize
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StreamInfo {
pub min_block_size: u16,
pub max_block_size: u16,
pub min_frame_size: u32,
pub max_frame_size: u32,
pub sample_rate: u32,
pub channels: u8,
pub bits_per_sample: u8,
pub total_samples: u64,
pub md5_signature: [u8; MD5_SIZE],
}
impl StreamInfo {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FlacError> {
if bytes.len() != STREAMINFO_SIZE {
return Err(FlacError::InvalidStreamInfoSize(bytes.len()));
}
let min_block_size = u16::from_be_bytes([bytes[0], bytes[1]]);
let max_block_size = u16::from_be_bytes([bytes[2], bytes[3]]);
let min_frame_size = u32::from_be_bytes([0, bytes[4], bytes[5], bytes[6]]);
let max_frame_size = u32::from_be_bytes([0, bytes[7], bytes[8], bytes[9]]);
let sample_rate =
((bytes[10] as u32) << 12) | ((bytes[11] as u32) << 4) | ((bytes[12] as u32) >> 4);
let channels = ((bytes[12] & 0x0E) >> 1) + 1;
let bits_per_sample = (((bytes[12] & 0x01) << 4) | ((bytes[13] & 0xF0) >> 4)) + 1;
let total_samples = ((bytes[13] as u64 & 0x0F) << 32)
| ((bytes[14] as u64) << 24)
| ((bytes[15] as u64) << 16)
| ((bytes[16] as u64) << 8)
| (bytes[17] as u64);
let mut md5_signature = [0u8; MD5_SIZE];
md5_signature.copy_from_slice(&bytes[18..34]);
Ok(StreamInfo {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5_signature,
})
}
pub fn to_bytes(&self) -> [u8; STREAMINFO_SIZE] {
let mut bytes = [0u8; STREAMINFO_SIZE];
bytes[0..2].copy_from_slice(&self.min_block_size.to_be_bytes());
bytes[2..4].copy_from_slice(&self.max_block_size.to_be_bytes());
let min_frame = self.min_frame_size.to_be_bytes();
bytes[4..7].copy_from_slice(&min_frame[1..4]);
let max_frame = self.max_frame_size.to_be_bytes();
bytes[7..10].copy_from_slice(&max_frame[1..4]);
let channels_minus_1 = self.channels - 1;
let bits_minus_1 = self.bits_per_sample - 1;
bytes[10] = (self.sample_rate >> 12) as u8;
bytes[11] = (self.sample_rate >> 4) as u8;
bytes[12] = ((self.sample_rate & 0x0F) << 4) as u8
| ((channels_minus_1 & 0x07) << 1)
| ((bits_minus_1 >> 4) & 0x01);
bytes[13] = ((bits_minus_1 & 0x0F) << 4) | ((self.total_samples >> 32) as u8 & 0x0F);
let total_lower = (self.total_samples & 0xFFFFFFFF) as u32;
bytes[14..18].copy_from_slice(&total_lower.to_be_bytes());
bytes[18..34].copy_from_slice(&self.md5_signature);
bytes
}
pub fn sample_type(&self) -> AudioIOResult<ValidatedSampleType> {
match self.bits_per_sample {
1..=16 => Ok(ValidatedSampleType::I16),
17..=24 => Ok(ValidatedSampleType::I24),
25..=32 => Ok(ValidatedSampleType::I32),
_ => Err(AudioIOError::corrupted_data_simple(
"Invalid bits per sample",
format!("{} bits", self.bits_per_sample),
)),
}
}
pub const fn block_align(&self) -> u16 {
let bytes_per_sample = (self.bits_per_sample as u16 + 7) / 8;
bytes_per_sample * self.channels as u16
}
pub const fn byte_rate(&self) -> u32 {
self.block_align() as u32 * self.sample_rate
}
pub const fn total_samples_all_channels(&self) -> u64 {
self.total_samples * self.channels as u64
}
pub fn has_md5(&self) -> bool {
self.md5_signature.iter().any(|&b| b != 0)
}
}
impl Display for StreamInfo {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
writeln!(f, "STREAMINFO:")?;
writeln!(
f,
" Block size: {}-{} samples",
self.min_block_size, self.max_block_size
)?;
writeln!(
f,
" Frame size: {}-{} bytes",
self.min_frame_size, self.max_frame_size
)?;
writeln!(f, " Sample rate: {} Hz", self.sample_rate)?;
writeln!(f, " Channels: {}", self.channels)?;
writeln!(f, " Bits per sample: {}", self.bits_per_sample)?;
writeln!(f, " Total samples: {}", self.total_samples)?;
write!(f, " MD5: ")?;
for b in &self.md5_signature {
write!(f, "{:02x}", b)?;
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeekPoint {
pub sample_number: u64,
pub stream_offset: u64,
pub frame_samples: u16,
}
impl SeekPoint {
pub const SIZE: usize = 18;
pub const PLACEHOLDER_SAMPLE: u64 = 0xFFFFFFFFFFFFFFFF;
pub fn from_bytes(bytes: &[u8; Self::SIZE]) -> Self {
let sample_number = u64::from_be_bytes([
bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
]);
let stream_offset = u64::from_be_bytes([
bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
]);
let frame_samples = u16::from_be_bytes([bytes[16], bytes[17]]);
SeekPoint {
sample_number,
stream_offset,
frame_samples,
}
}
pub fn to_bytes(&self) -> [u8; Self::SIZE] {
let mut bytes = [0u8; Self::SIZE];
bytes[0..8].copy_from_slice(&self.sample_number.to_be_bytes());
bytes[8..16].copy_from_slice(&self.stream_offset.to_be_bytes());
bytes[16..18].copy_from_slice(&self.frame_samples.to_be_bytes());
bytes
}
pub const fn is_placeholder(&self) -> bool {
self.sample_number == Self::PLACEHOLDER_SAMPLE
}
}
#[derive(Debug, Clone, Default)]
pub struct SeekTable {
pub points: Vec<SeekPoint>,
}
impl SeekTable {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FlacError> {
if bytes.len() % SeekPoint::SIZE != 0 {
return Err(FlacError::InvalidMetadataBlockSize {
size: bytes.len() as u32,
});
}
let num_points = bytes.len() / SeekPoint::SIZE;
let mut points = Vec::with_capacity(num_points);
for i in 0..num_points {
let start = i * SeekPoint::SIZE;
let point_bytes: &[u8; SeekPoint::SIZE] = bytes[start..start + SeekPoint::SIZE]
.try_into()
.map_err(|_| FlacError::UnexpectedEof)?;
points.push(SeekPoint::from_bytes(point_bytes));
}
Ok(SeekTable { points })
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(self.points.len() * SeekPoint::SIZE);
for point in &self.points {
bytes.extend_from_slice(&point.to_bytes());
}
bytes
}
pub fn find_seek_point(&self, sample: u64) -> Option<&SeekPoint> {
self.points
.iter()
.filter(|p| !p.is_placeholder() && p.sample_number <= sample)
.max_by_key(|p| p.sample_number)
}
pub fn validate(&self, total_samples: u64) -> Result<(), FlacError> {
let mut prev_sample = None;
for point in &self.points {
if point.is_placeholder() {
continue;
}
if let Some(prev) = prev_sample {
if point.sample_number <= prev {
return Err(FlacError::SeekTableNotSorted);
}
}
if point.sample_number > total_samples {
return Err(FlacError::InvalidSeekPoint {
sample: point.sample_number,
total: total_samples,
});
}
prev_sample = Some(point.sample_number);
}
Ok(())
}
}
#[derive(Debug, Clone, Default)]
pub struct VorbisComment {
pub vendor: String,
pub comments: HashMap<String, Vec<String>>,
}
impl VorbisComment {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FlacError> {
if bytes.len() < 8 {
return Err(FlacError::UnexpectedEof);
}
let mut pos = 0;
let vendor_len =
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
if pos + vendor_len > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let vendor = String::from_utf8_lossy(&bytes[pos..pos + vendor_len]).to_string();
pos += vendor_len;
if pos + 4 > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let num_comments =
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
let mut comments: HashMap<String, Vec<String>> = HashMap::new();
for _ in 0..num_comments {
if pos + 4 > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let comment_len =
u32::from_le_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
if pos + comment_len > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let comment = String::from_utf8_lossy(&bytes[pos..pos + comment_len]);
pos += comment_len;
if let Some(eq_pos) = comment.find('=') {
let key = comment[..eq_pos].to_uppercase();
let value = comment[eq_pos + 1..].to_string();
comments.entry(key).or_default().push(value);
}
}
Ok(VorbisComment { vendor, comments })
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
let vendor_bytes = self.vendor.as_bytes();
bytes.extend_from_slice(&(vendor_bytes.len() as u32).to_le_bytes());
bytes.extend_from_slice(vendor_bytes);
let total_comments: usize = self.comments.values().map(|v| v.len()).sum();
bytes.extend_from_slice(&(total_comments as u32).to_le_bytes());
for (key, values) in &self.comments {
for value in values {
let comment = format!("{}={}", key, value);
let comment_bytes = comment.as_bytes();
bytes.extend_from_slice(&(comment_bytes.len() as u32).to_le_bytes());
bytes.extend_from_slice(comment_bytes);
}
}
bytes
}
pub fn get(&self, key: &str) -> Option<&str> {
self.comments
.get(&key.to_uppercase())
.and_then(|v| v.first())
.map(|s| s.as_str())
}
pub fn get_all(&self, key: &str) -> Option<&Vec<String>> {
self.comments.get(&key.to_uppercase())
}
pub fn set(&mut self, key: &str, value: impl Into<String>) {
self.comments.insert(key.to_uppercase(), vec![value.into()]);
}
pub fn add(&mut self, key: &str, value: impl Into<String>) {
self.comments
.entry(key.to_uppercase())
.or_default()
.push(value.into());
}
}
impl Display for VorbisComment {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
writeln!(f, "VORBIS_COMMENT:")?;
writeln!(f, " Vendor: {}", self.vendor)?;
for (key, values) in &self.comments {
for value in values {
writeln!(f, " {}={}", key, value)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum PictureType {
Other = 0,
FileIcon = 1,
OtherFileIcon = 2,
FrontCover = 3,
BackCover = 4,
LeafletPage = 5,
Media = 6,
LeadArtist = 7,
Artist = 8,
Conductor = 9,
Band = 10,
Composer = 11,
Lyricist = 12,
RecordingLocation = 13,
DuringRecording = 14,
DuringPerformance = 15,
ScreenCapture = 16,
BrightFish = 17,
Illustration = 18,
BandLogo = 19,
PublisherLogo = 20,
}
impl PictureType {
pub const fn from_u32(value: u32) -> Self {
match value {
0 => PictureType::Other,
1 => PictureType::FileIcon,
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::Band,
11 => PictureType::Composer,
12 => PictureType::Lyricist,
13 => PictureType::RecordingLocation,
14 => PictureType::DuringRecording,
15 => PictureType::DuringPerformance,
16 => PictureType::ScreenCapture,
17 => PictureType::BrightFish,
18 => PictureType::Illustration,
19 => PictureType::BandLogo,
20 => PictureType::PublisherLogo,
_ => PictureType::Other,
}
}
}
#[derive(Debug, Clone)]
pub struct Picture {
pub picture_type: PictureType,
pub mime_type: String,
pub description: String,
pub width: u32,
pub height: u32,
pub color_depth: u32,
pub num_colors: u32,
pub data: Vec<u8>,
}
impl Picture {
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FlacError> {
if bytes.len() < 32 {
return Err(FlacError::UnexpectedEof);
}
let mut pos = 0;
let picture_type = PictureType::from_u32(u32::from_be_bytes([
bytes[pos],
bytes[pos + 1],
bytes[pos + 2],
bytes[pos + 3],
]));
pos += 4;
let mime_len =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
if pos + mime_len > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let mime_type = String::from_utf8_lossy(&bytes[pos..pos + mime_len]).to_string();
pos += mime_len;
if pos + 4 > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let desc_len =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
if pos + desc_len > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let description = String::from_utf8_lossy(&bytes[pos..pos + desc_len]).to_string();
pos += desc_len;
if pos + 20 > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let width =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
pos += 4;
let height =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
pos += 4;
let color_depth =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
pos += 4;
let num_colors =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]]);
pos += 4;
let data_len =
u32::from_be_bytes([bytes[pos], bytes[pos + 1], bytes[pos + 2], bytes[pos + 3]])
as usize;
pos += 4;
if pos + data_len > bytes.len() {
return Err(FlacError::UnexpectedEof);
}
let data = bytes[pos..pos + data_len].to_vec();
Ok(Picture {
picture_type,
mime_type,
description,
width,
height,
color_depth,
num_colors,
data,
})
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::new();
bytes.extend_from_slice(&(self.picture_type as u32).to_be_bytes());
let mime_bytes = self.mime_type.as_bytes();
bytes.extend_from_slice(&(mime_bytes.len() as u32).to_be_bytes());
bytes.extend_from_slice(mime_bytes);
let desc_bytes = self.description.as_bytes();
bytes.extend_from_slice(&(desc_bytes.len() as u32).to_be_bytes());
bytes.extend_from_slice(desc_bytes);
bytes.extend_from_slice(&self.width.to_be_bytes());
bytes.extend_from_slice(&self.height.to_be_bytes());
bytes.extend_from_slice(&self.color_depth.to_be_bytes());
bytes.extend_from_slice(&self.num_colors.to_be_bytes());
bytes.extend_from_slice(&(self.data.len() as u32).to_be_bytes());
bytes.extend_from_slice(&self.data);
bytes
}
}
#[derive(Debug, Clone)]
pub enum MetadataBlock {
StreamInfo(StreamInfo),
Padding(usize),
Application { id: [u8; 4], data: Vec<u8> },
SeekTable(SeekTable),
VorbisComment(VorbisComment),
CueSheet(Vec<u8>), Picture(Picture),
Unknown { block_type: u8, data: Vec<u8> },
}
impl MetadataBlock {
pub fn parse(header: &MetadataBlockHeader, data: &[u8]) -> Result<Self, FlacError> {
match header.block_type {
MetadataBlockType::StreamInfo => {
Ok(MetadataBlock::StreamInfo(StreamInfo::from_bytes(data)?))
}
MetadataBlockType::Padding => Ok(MetadataBlock::Padding(data.len())),
MetadataBlockType::Application => {
if data.len() < 4 {
return Err(FlacError::UnexpectedEof);
}
let mut id = [0u8; 4];
id.copy_from_slice(&data[0..4]);
Ok(MetadataBlock::Application {
id,
data: data[4..].to_vec(),
})
}
MetadataBlockType::SeekTable => {
Ok(MetadataBlock::SeekTable(SeekTable::from_bytes(data)?))
}
MetadataBlockType::VorbisComment => Ok(MetadataBlock::VorbisComment(
VorbisComment::from_bytes(data)?,
)),
MetadataBlockType::CueSheet => Ok(MetadataBlock::CueSheet(data.to_vec())),
MetadataBlockType::Picture => Ok(MetadataBlock::Picture(Picture::from_bytes(data)?)),
MetadataBlockType::Reserved(n) => Ok(MetadataBlock::Unknown {
block_type: n,
data: data.to_vec(),
}),
}
}
pub fn block_type(&self) -> MetadataBlockType {
match self {
MetadataBlock::StreamInfo(_) => MetadataBlockType::StreamInfo,
MetadataBlock::Padding(_) => MetadataBlockType::Padding,
MetadataBlock::Application { .. } => MetadataBlockType::Application,
MetadataBlock::SeekTable(_) => MetadataBlockType::SeekTable,
MetadataBlock::VorbisComment(_) => MetadataBlockType::VorbisComment,
MetadataBlock::CueSheet(_) => MetadataBlockType::CueSheet,
MetadataBlock::Picture(_) => MetadataBlockType::Picture,
MetadataBlock::Unknown { block_type, .. } => MetadataBlockType::Reserved(*block_type),
}
}
pub fn to_bytes(&self) -> Vec<u8> {
match self {
MetadataBlock::StreamInfo(info) => info.to_bytes().to_vec(),
MetadataBlock::Padding(size) => vec![0u8; *size],
MetadataBlock::Application { id, data } => {
let mut bytes = id.to_vec();
bytes.extend_from_slice(data);
bytes
}
MetadataBlock::SeekTable(table) => table.to_bytes(),
MetadataBlock::VorbisComment(comment) => comment.to_bytes(),
MetadataBlock::CueSheet(data) => data.clone(),
MetadataBlock::Picture(picture) => picture.to_bytes(),
MetadataBlock::Unknown { data, .. } => data.clone(),
}
}
}
#[derive(Debug, Clone)]
pub struct FlacMetadata {
pub stream_info: StreamInfo,
pub blocks: Vec<MetadataBlock>,
pub audio_offset: usize,
}
impl FlacMetadata {
pub fn parse(data: &[u8], start_offset: usize) -> AudioIOResult<Self> {
let mut offset = start_offset;
let mut blocks = Vec::new();
let mut stream_info: Option<StreamInfo> = None;
loop {
if offset + 4 > data.len() {
return Err(AudioIOError::corrupted_data(
"Unexpected end of metadata",
"Not enough bytes for metadata block header",
ErrorPosition::new(offset),
));
}
let header_bytes: [u8; 4] = data[offset..offset + 4]
.try_into()
.map_err(|_| FlacError::UnexpectedEof)?;
let header = MetadataBlockHeader::from_bytes(&header_bytes);
offset += 4;
if header.length > 16 * 1024 * 1024 {
return Err(AudioIOError::from(FlacError::InvalidMetadataBlockSize {
size: header.length,
}));
}
if offset + header.length as usize > data.len() {
return Err(AudioIOError::corrupted_data(
"Metadata block extends beyond file",
format!(
"Block {} at offset {}, length {} exceeds file size {}",
header.block_type,
offset - 4,
header.length,
data.len()
),
ErrorPosition::new(offset - 4),
));
}
let block_data = &data[offset..offset + header.length as usize];
let block = MetadataBlock::parse(&header, block_data).map_err(AudioIOError::from)?;
if let MetadataBlock::StreamInfo(ref info) = block {
stream_info = Some(*info);
}
blocks.push(block);
offset += header.length as usize;
if header.is_last {
break;
}
}
let stream_info =
stream_info.ok_or_else(|| AudioIOError::from(FlacError::MissingStreamInfo))?;
Ok(FlacMetadata {
stream_info,
blocks,
audio_offset: offset,
})
}
pub fn seek_table(&self) -> Option<&SeekTable> {
self.blocks.iter().find_map(|b| {
if let MetadataBlock::SeekTable(table) = b {
Some(table)
} else {
None
}
})
}
pub fn vorbis_comment(&self) -> Option<&VorbisComment> {
self.blocks.iter().find_map(|b| {
if let MetadataBlock::VorbisComment(comment) = b {
Some(comment)
} else {
None
}
})
}
pub fn pictures(&self) -> Vec<&Picture> {
self.blocks
.iter()
.filter_map(|b| {
if let MetadataBlock::Picture(pic) = b {
Some(pic)
} else {
None
}
})
.collect()
}
}
impl AudioInfoMarker for FlacMetadata {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_streaminfo_roundtrip() {
let info = StreamInfo {
min_block_size: 4096,
max_block_size: 4096,
min_frame_size: 1234,
max_frame_size: 5678,
sample_rate: 44100,
channels: 2,
bits_per_sample: 16,
total_samples: 441000,
md5_signature: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
};
let bytes = info.to_bytes();
assert_eq!(bytes.len(), STREAMINFO_SIZE);
let parsed = StreamInfo::from_bytes(&bytes).expect("Parse failed");
assert_eq!(parsed.min_block_size, info.min_block_size);
assert_eq!(parsed.max_block_size, info.max_block_size);
assert_eq!(parsed.min_frame_size, info.min_frame_size);
assert_eq!(parsed.max_frame_size, info.max_frame_size);
assert_eq!(parsed.sample_rate, info.sample_rate);
assert_eq!(parsed.channels, info.channels);
assert_eq!(parsed.bits_per_sample, info.bits_per_sample);
assert_eq!(parsed.total_samples, info.total_samples);
assert_eq!(parsed.md5_signature, info.md5_signature);
}
#[test]
fn test_seekpoint_roundtrip() {
let point = SeekPoint {
sample_number: 0x123456789ABCDEF0,
stream_offset: 0xFEDCBA9876543210,
frame_samples: 4096,
};
let bytes = point.to_bytes();
let parsed = SeekPoint::from_bytes(&bytes);
assert_eq!(parsed.sample_number, point.sample_number);
assert_eq!(parsed.stream_offset, point.stream_offset);
assert_eq!(parsed.frame_samples, point.frame_samples);
}
#[test]
fn test_vorbis_comment_roundtrip() {
let mut comment = VorbisComment {
vendor: "audio_samples_io/0.1.0".to_string(),
comments: HashMap::new(),
};
comment.set("TITLE", "Test Song");
comment.set("ARTIST", "Test Artist");
comment.add("GENRE", "Electronic");
comment.add("GENRE", "Ambient");
let bytes = comment.to_bytes();
let parsed = VorbisComment::from_bytes(&bytes).expect("Parse failed");
assert_eq!(parsed.vendor, comment.vendor);
assert_eq!(parsed.get("TITLE"), Some("Test Song"));
assert_eq!(parsed.get("ARTIST"), Some("Test Artist"));
assert_eq!(parsed.get_all("GENRE").map(|v| v.len()), Some(2));
}
#[test]
fn test_metadata_block_header() {
let header = MetadataBlockHeader {
is_last: true,
block_type: MetadataBlockType::StreamInfo,
length: 34,
};
let bytes = header.to_bytes();
assert_eq!(bytes[0], 0x80); assert_eq!(bytes[1], 0);
assert_eq!(bytes[2], 0);
assert_eq!(bytes[3], 34);
let parsed = MetadataBlockHeader::from_bytes(&bytes);
assert!(parsed.is_last);
assert_eq!(parsed.block_type, MetadataBlockType::StreamInfo);
assert_eq!(parsed.length, 34);
}
}