use oximedia_core::{OxiError, OxiResult};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BlockType {
StreamInfo,
Padding,
Application,
SeekTable,
VorbisComment,
CueSheet,
Picture,
Reserved,
}
impl From<u8> for BlockType {
fn from(value: u8) -> Self {
match value & 0x7F {
0 => Self::StreamInfo,
1 => Self::Padding,
2 => Self::Application,
3 => Self::SeekTable,
4 => Self::VorbisComment,
5 => Self::CueSheet,
6 => Self::Picture,
_ => Self::Reserved,
}
}
}
impl BlockType {
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::StreamInfo => 0,
Self::Padding => 1,
Self::Application => 2,
Self::SeekTable => 3,
Self::VorbisComment => 4,
Self::CueSheet => 5,
Self::Picture => 6,
Self::Reserved => 127,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::StreamInfo => "STREAMINFO",
Self::Padding => "PADDING",
Self::Application => "APPLICATION",
Self::SeekTable => "SEEKTABLE",
Self::VorbisComment => "VORBIS_COMMENT",
Self::CueSheet => "CUESHEET",
Self::Picture => "PICTURE",
Self::Reserved => "RESERVED",
}
}
}
#[derive(Clone, Debug)]
pub struct MetadataBlock {
pub is_last: bool,
pub block_type: BlockType,
pub length: u32,
pub data: Vec<u8>,
}
impl MetadataBlock {
pub fn parse(input: &[u8]) -> OxiResult<(Self, usize)> {
if input.len() < 4 {
return Err(OxiError::UnexpectedEof);
}
let is_last = input[0] & 0x80 != 0;
let block_type = BlockType::from(input[0]);
let length = u32::from_be_bytes([0, input[1], input[2], input[3]]);
let header_size = 4;
let total_size = header_size + length as usize;
if input.len() < total_size {
return Err(OxiError::UnexpectedEof);
}
let data = input[header_size..total_size].to_vec();
Ok((
Self {
is_last,
block_type,
length,
data,
},
total_size,
))
}
#[must_use]
pub const fn is_last(&self) -> bool {
self.is_last
}
#[must_use]
pub const fn total_size(&self) -> usize {
4 + self.length as usize
}
}
#[derive(Clone, Debug)]
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: [u8; 16],
}
impl StreamInfo {
pub const SIZE: usize = 34;
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() != Self::SIZE {
return Err(OxiError::Parse {
offset: 0,
message: format!(
"STREAMINFO must be {} bytes, got {}",
Self::SIZE,
data.len()
),
});
}
let min_block_size = u16::from_be_bytes([data[0], data[1]]);
let max_block_size = u16::from_be_bytes([data[2], data[3]]);
let min_frame_size = u32::from_be_bytes([0, data[4], data[5], data[6]]);
let max_frame_size = u32::from_be_bytes([0, data[7], data[8], data[9]]);
let sample_rate =
(u32::from(data[10]) << 12) | (u32::from(data[11]) << 4) | (u32::from(data[12]) >> 4);
let channels = ((data[12] >> 1) & 0x07) + 1;
let bits_per_sample = (((data[12] & 0x01) << 4) | (data[13] >> 4)) + 1;
let total_samples = (u64::from(data[13] & 0x0F) << 32)
| (u64::from(data[14]) << 24)
| (u64::from(data[15]) << 16)
| (u64::from(data[16]) << 8)
| u64::from(data[17]);
let mut md5 = [0u8; 16];
md5.copy_from_slice(&data[18..34]);
Ok(Self {
min_block_size,
max_block_size,
min_frame_size,
max_frame_size,
sample_rate,
channels,
bits_per_sample,
total_samples,
md5,
})
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn duration_seconds(&self) -> Option<f64> {
if self.total_samples == 0 || self.sample_rate == 0 {
None
} else {
Some(self.total_samples as f64 / f64::from(self.sample_rate))
}
}
#[must_use]
pub fn bytes_per_sample(&self) -> u8 {
self.bits_per_sample.div_ceil(8)
}
#[must_use]
pub fn has_md5(&self) -> bool {
self.md5 != [0u8; 16]
}
}
#[derive(Clone, Debug, Default)]
pub struct VorbisComment {
pub vendor: String,
pub comments: Vec<(String, String)>,
}
impl VorbisComment {
pub fn parse(data: &[u8]) -> OxiResult<Self> {
if data.len() < 8 {
return Err(OxiError::UnexpectedEof);
}
let mut offset = 0;
let vendor_len = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
offset += 4;
if offset + vendor_len > data.len() {
return Err(OxiError::UnexpectedEof);
}
let vendor = String::from_utf8_lossy(&data[offset..offset + vendor_len]).into_owned();
offset += vendor_len;
if offset + 4 > data.len() {
return Err(OxiError::UnexpectedEof);
}
let comment_count = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
offset += 4;
let mut comments = Vec::with_capacity(comment_count.min(1000));
for _ in 0..comment_count {
if offset + 4 > data.len() {
break;
}
let comment_len = u32::from_le_bytes([
data[offset],
data[offset + 1],
data[offset + 2],
data[offset + 3],
]) as usize;
offset += 4;
if offset + comment_len > data.len() {
break;
}
let comment = String::from_utf8_lossy(&data[offset..offset + comment_len]);
offset += comment_len;
if let Some((key, value)) = comment.split_once('=') {
comments.push((key.to_uppercase(), value.to_string()));
}
}
Ok(Self { vendor, comments })
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&str> {
let key_upper = key.to_uppercase();
self.comments
.iter()
.find(|(k, _)| k == &key_upper)
.map(|(_, v)| v.as_str())
}
#[must_use]
pub fn get_all(&self, key: &str) -> Vec<&str> {
let key_upper = key.to_uppercase();
self.comments
.iter()
.filter(|(k, _)| k == &key_upper)
.map(|(_, v)| v.as_str())
.collect()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.comments.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.comments.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
self.comments.iter().map(|(k, v)| (k.as_str(), v.as_str()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_block_type_from_u8() {
assert_eq!(BlockType::from(0), BlockType::StreamInfo);
assert_eq!(BlockType::from(1), BlockType::Padding);
assert_eq!(BlockType::from(2), BlockType::Application);
assert_eq!(BlockType::from(3), BlockType::SeekTable);
assert_eq!(BlockType::from(4), BlockType::VorbisComment);
assert_eq!(BlockType::from(5), BlockType::CueSheet);
assert_eq!(BlockType::from(6), BlockType::Picture);
assert_eq!(BlockType::from(7), BlockType::Reserved);
assert_eq!(BlockType::from(127), BlockType::Reserved);
}
#[test]
fn test_block_type_with_last_flag() {
assert_eq!(BlockType::from(0x80), BlockType::StreamInfo);
assert_eq!(BlockType::from(0x84), BlockType::VorbisComment);
}
#[test]
fn test_block_type_as_u8() {
assert_eq!(BlockType::StreamInfo.as_u8(), 0);
assert_eq!(BlockType::VorbisComment.as_u8(), 4);
assert_eq!(BlockType::Reserved.as_u8(), 127);
}
#[test]
fn test_block_type_name() {
assert_eq!(BlockType::StreamInfo.name(), "STREAMINFO");
assert_eq!(BlockType::VorbisComment.name(), "VORBIS_COMMENT");
}
#[test]
fn test_metadata_block_parse() {
let mut data = vec![0x01]; data.extend_from_slice(&[0x00, 0x00, 0x08]); data.extend_from_slice(&[0x00; 8]);
let (block, consumed) = MetadataBlock::parse(&data).expect("operation should succeed");
assert!(!block.is_last);
assert_eq!(block.block_type, BlockType::Padding);
assert_eq!(block.length, 8);
assert_eq!(block.data.len(), 8);
assert_eq!(consumed, 12); }
#[test]
fn test_metadata_block_is_last() {
let data = vec![0x81, 0x00, 0x00, 0x00];
let (block, _) = MetadataBlock::parse(&data).expect("operation should succeed");
assert!(block.is_last);
assert!(block.is_last());
}
#[test]
fn test_stream_info_parse() {
let mut data = vec![0u8; 34];
data[0] = 0x10;
data[1] = 0x00;
data[2] = 0x10;
data[3] = 0x00;
data[10] = 0x0A;
data[11] = 0xC4;
data[12] = 0x42;
data[13] = 0xF0;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert_eq!(info.min_block_size, 4096);
assert_eq!(info.max_block_size, 4096);
assert_eq!(info.sample_rate, 44100);
assert_eq!(info.channels, 2);
assert_eq!(info.bits_per_sample, 16);
assert_eq!(info.total_samples, 0);
}
#[test]
fn test_stream_info_wrong_size() {
let data = vec![0u8; 33]; assert!(StreamInfo::parse(&data).is_err());
let data = vec![0u8; 35]; assert!(StreamInfo::parse(&data).is_err());
}
#[test]
fn test_stream_info_duration() {
let mut data = vec![0u8; 34];
data[10] = 0x0A; data[11] = 0xC4; data[12] = 0x42; data[13] = 0xF0;
data[14] = 0x00;
data[15] = 0x06;
data[16] = 0xBA;
data[17] = 0xA8;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert_eq!(info.sample_rate, 44100);
assert_eq!(info.total_samples, 441000);
let duration = info.duration_seconds().expect("operation should succeed");
assert!((duration - 10.0).abs() < 0.001);
}
#[test]
fn test_stream_info_no_duration() {
let mut data = vec![0u8; 34];
data[10] = 0x0A;
data[11] = 0xC4;
data[12] = 0x42;
data[13] = 0xF0;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert!(info.duration_seconds().is_none());
}
#[test]
fn test_stream_info_bytes_per_sample() {
let mut data = vec![0u8; 34];
data[10] = 0x0A;
data[11] = 0xC4;
data[12] = 0x42;
data[13] = 0xF0;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert_eq!(info.bytes_per_sample(), 2);
}
#[test]
fn test_stream_info_has_md5() {
let mut data = vec![0u8; 34];
data[10] = 0x0A;
data[11] = 0xC4;
data[12] = 0x42;
data[13] = 0xF0;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert!(!info.has_md5());
data[18] = 0x01;
let info = StreamInfo::parse(&data).expect("operation should succeed");
assert!(info.has_md5());
}
#[test]
fn test_vorbis_comment_parse() {
let mut data = Vec::new();
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(b"Test");
data.extend_from_slice(&2u32.to_le_bytes());
let comment1 = b"ARTIST=Test Artist";
data.extend_from_slice(&(comment1.len() as u32).to_le_bytes());
data.extend_from_slice(comment1);
let comment2 = b"TITLE=Test Title";
data.extend_from_slice(&(comment2.len() as u32).to_le_bytes());
data.extend_from_slice(comment2);
let vc = VorbisComment::parse(&data).expect("operation should succeed");
assert_eq!(vc.vendor, "Test");
assert_eq!(vc.len(), 2);
assert_eq!(vc.get("ARTIST"), Some("Test Artist"));
assert_eq!(vc.get("TITLE"), Some("Test Title"));
assert_eq!(vc.get("artist"), Some("Test Artist")); }
#[test]
fn test_vorbis_comment_empty() {
let mut data = Vec::new();
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&0u32.to_le_bytes());
let vc = VorbisComment::parse(&data).expect("operation should succeed");
assert!(vc.vendor.is_empty());
assert!(vc.is_empty());
assert_eq!(vc.len(), 0);
}
#[test]
fn test_vorbis_comment_get_all() {
let mut data = Vec::new();
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&2u32.to_le_bytes());
let genre1 = b"GENRE=Rock";
data.extend_from_slice(&(genre1.len() as u32).to_le_bytes());
data.extend_from_slice(genre1);
let genre2 = b"GENRE=Alternative";
data.extend_from_slice(&(genre2.len() as u32).to_le_bytes());
data.extend_from_slice(genre2);
let vc = VorbisComment::parse(&data).expect("operation should succeed");
let genres = vc.get_all("GENRE");
assert_eq!(genres.len(), 2);
assert!(genres.contains(&"Rock"));
assert!(genres.contains(&"Alternative"));
}
#[test]
fn test_vorbis_comment_iter() {
let mut data = Vec::new();
data.extend_from_slice(&0u32.to_le_bytes());
data.extend_from_slice(&1u32.to_le_bytes());
let comment = b"KEY=value";
data.extend_from_slice(&(comment.len() as u32).to_le_bytes());
data.extend_from_slice(comment);
let vc = VorbisComment::parse(&data).expect("operation should succeed");
let entries: Vec<_> = vc.iter().collect();
assert_eq!(entries.len(), 1);
assert_eq!(entries[0], ("KEY", "value"));
}
#[test]
fn test_vorbis_comment_too_short() {
let data = vec![0u8; 4]; assert!(VorbisComment::parse(&data).is_err());
}
}