const MPEG1_L3_BITRATES: [u16; 16] = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
];
const MPEG1_SAMPLE_RATES: [u32; 4] = [44100, 48000, 32000, 0];
pub const REQUIRED_ID3_FRAMES: &[&[u8; 4]] = &[
b"TIT2", b"TPE1", b"TALB", b"TRCK", ];
#[derive(Debug, Clone)]
pub struct CbrReport {
pub is_cbr: bool,
pub detected_bitrate_kbps: Option<u16>,
pub vbr_frame_count: usize,
pub total_frames: usize,
}
#[derive(Debug, Clone)]
pub struct Id3Report {
pub complete: bool,
pub missing: Vec<String>,
}
pub fn check_cbr(mp3_bytes: &[u8]) -> CbrReport {
let mut i = 0usize;
let mut first_bitrate: Option<u16> = None;
let mut vbr_count = 0usize;
let mut total = 0usize;
while i + 4 <= mp3_bytes.len() {
if mp3_bytes[i] != 0xFF || (mp3_bytes[i + 1] & 0xE0) != 0xE0 {
i += 1;
continue;
}
let h1 = mp3_bytes[i + 1];
let h2 = mp3_bytes[i + 2];
let mpeg_version = (h1 >> 3) & 0x03;
let layer = (h1 >> 1) & 0x03;
if mpeg_version != 3 || layer != 1 {
i += 1;
continue;
}
let bitrate_idx = ((h2 >> 4) & 0x0F) as usize;
let sample_rate_idx = ((h2 >> 2) & 0x03) as usize;
let padding = (h2 >> 1) & 0x01;
let bitrate = MPEG1_L3_BITRATES[bitrate_idx];
let sample_rate = MPEG1_SAMPLE_RATES[sample_rate_idx];
if bitrate == 0 || sample_rate == 0 {
i += 1;
continue;
}
total += 1;
match first_bitrate {
None => first_bitrate = Some(bitrate),
Some(br) if br != bitrate => vbr_count += 1,
_ => {}
}
let frame_size = 144 * bitrate as usize * 1000 / sample_rate as usize + padding as usize;
i += frame_size.max(1);
}
CbrReport {
is_cbr: vbr_count == 0,
detected_bitrate_kbps: first_bitrate,
vbr_frame_count: vbr_count,
total_frames: total,
}
}
pub fn check_id3_tags(mp3_bytes: &[u8]) -> Id3Report {
if mp3_bytes.len() < 10 || &mp3_bytes[..3] != b"ID3" {
let missing = REQUIRED_ID3_FRAMES
.iter()
.map(|id| String::from_utf8_lossy(*id).into_owned())
.collect();
return Id3Report {
complete: false,
missing,
};
}
let tag_size = syncsafe_to_u32(&mp3_bytes[6..10]) as usize + 10;
let tag_region = &mp3_bytes[..tag_size.min(mp3_bytes.len())];
let mut missing = Vec::new();
for &required_id in REQUIRED_ID3_FRAMES {
if !tag_region.windows(4).any(|w| w == required_id.as_slice()) {
missing.push(String::from_utf8_lossy(required_id).into_owned());
}
}
Id3Report {
complete: missing.is_empty(),
missing,
}
}
fn syncsafe_to_u32(bytes: &[u8]) -> u32 {
(bytes[0] as u32) << 21 | (bytes[1] as u32) << 14 | (bytes[2] as u32) << 7 | bytes[3] as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_mp3_reports_no_frames() {
let report = check_cbr(&[]);
assert_eq!(report.total_frames, 0);
assert!(report.is_cbr); assert!(report.detected_bitrate_kbps.is_none());
}
#[test]
fn no_id3_tag_reports_all_missing() {
let report = check_id3_tags(&[0xFF, 0xFB, 0x90, 0x00]);
assert!(!report.complete);
assert_eq!(report.missing.len(), REQUIRED_ID3_FRAMES.len());
}
#[test]
fn minimal_id3_tag_with_all_frames_passes() {
let mut tag = b"ID3".to_vec();
tag.extend_from_slice(&[3, 0, 0]);
let mut frames = Vec::new();
for &frame_id in REQUIRED_ID3_FRAMES {
frames.extend_from_slice(frame_id.as_slice());
frames.extend_from_slice(&[0, 0, 0, 2]); frames.extend_from_slice(&[0, 0]); frames.extend_from_slice(&[0, b'X']); }
let size = frames.len() as u32;
tag.push(((size >> 21) & 0x7F) as u8);
tag.push(((size >> 14) & 0x7F) as u8);
tag.push(((size >> 7) & 0x7F) as u8);
tag.push((size & 0x7F) as u8);
tag.extend_from_slice(&frames);
let report = check_id3_tags(&tag);
assert!(
report.complete,
"Expected complete ID3 tag, missing: {:?}",
report.missing
);
}
#[test]
fn id3_tag_missing_track_number_reports_it() {
let mut tag = b"ID3".to_vec();
tag.extend_from_slice(&[3, 0, 0]);
let mut frames = Vec::new();
for &frame_id in &[b"TIT2", b"TPE1", b"TALB"] {
frames.extend_from_slice(frame_id.as_slice());
frames.extend_from_slice(&[0, 0, 0, 2]);
frames.extend_from_slice(&[0, 0, 0, b'X']);
}
let size = frames.len() as u32;
tag.push(((size >> 21) & 0x7F) as u8);
tag.push(((size >> 14) & 0x7F) as u8);
tag.push(((size >> 7) & 0x7F) as u8);
tag.push((size & 0x7F) as u8);
tag.extend_from_slice(&frames);
let report = check_id3_tags(&tag);
assert!(!report.complete);
assert!(report.missing.contains(&"TRCK".to_string()));
}
#[test]
fn syncsafe_integer_decodes_correctly() {
assert_eq!(syncsafe_to_u32(&[0x00, 0x00, 0x02, 0x01]), 257);
}
}