use crate::analysis::{ChannelMode, MpegVersion};
use crate::error::{Error, Result};
pub(crate) const APE_PREAMBLE: &[u8; 8] = b"APETAGEX";
pub(crate) const APE_FLAG_HEADER_PRESENT: u32 = 1 << 31;
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub(crate) struct FrameHeader {
pub version: MpegVersion,
pub has_crc: bool,
pub bitrate_kbps: u32,
pub sample_rate: u32,
pub padding: bool,
pub channel_mode: ChannelMode,
pub frame_size: usize,
}
impl FrameHeader {
pub fn granule_count(&self) -> usize {
match self.version {
MpegVersion::Mpeg1 => 2,
_ => 1,
}
}
pub fn side_info_offset(&self) -> usize {
if self.has_crc {
6
} else {
4
}
}
}
const BITRATE_TABLE_MPEG1_L3: [u32; 15] = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
];
const BITRATE_TABLE_MPEG2_L3: [u32; 15] =
[0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160];
const SAMPLE_RATE_TABLE: [[u32; 3]; 3] = [
[44100, 48000, 32000], [22050, 24000, 16000], [11025, 12000, 8000], ];
pub(crate) fn parse_header(header: &[u8]) -> Option<FrameHeader> {
if header.len() < 4 {
return None;
}
if header[0] != 0xFF || (header[1] & 0xE0) != 0xE0 {
return None;
}
let version_bits = (header[1] >> 3) & 0x03;
let version = match version_bits {
0b00 => MpegVersion::Mpeg25,
0b10 => MpegVersion::Mpeg2,
0b11 => MpegVersion::Mpeg1,
_ => return None,
};
let layer_bits = (header[1] >> 1) & 0x03;
if layer_bits != 0b01 {
return None;
}
let has_crc = (header[1] & 0x01) == 0;
let bitrate_index = (header[2] >> 4) & 0x0F;
if bitrate_index == 0 || bitrate_index == 15 {
return None;
}
let bitrate_kbps = match version {
MpegVersion::Mpeg1 => BITRATE_TABLE_MPEG1_L3[bitrate_index as usize],
_ => BITRATE_TABLE_MPEG2_L3[bitrate_index as usize],
};
let sr_index = ((header[2] >> 2) & 0x03) as usize;
if sr_index == 3 {
return None;
}
let version_index = match version {
MpegVersion::Mpeg1 => 0,
MpegVersion::Mpeg2 => 1,
MpegVersion::Mpeg25 => 2,
};
let sample_rate = SAMPLE_RATE_TABLE[version_index][sr_index];
let padding = (header[2] & 0x02) != 0;
let channel_bits = (header[3] >> 6) & 0x03;
let channel_mode = match channel_bits {
0b00 => ChannelMode::Stereo,
0b01 => ChannelMode::JointStereo,
0b10 => ChannelMode::DualChannel,
0b11 => ChannelMode::Mono,
_ => unreachable!(),
};
let samples_per_frame = match version {
MpegVersion::Mpeg1 => 1152,
_ => 576,
};
let padding_size = if padding { 1 } else { 0 };
let frame_size =
(samples_per_frame * bitrate_kbps as usize * 125) / sample_rate as usize + padding_size;
Some(FrameHeader {
version,
has_crc,
bitrate_kbps,
sample_rate,
padding,
channel_mode,
frame_size,
})
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct GainLocation {
pub byte_offset: usize,
pub bit_offset: u8,
}
pub(crate) const MAX_GAIN_LOCATIONS: usize = 4;
pub(crate) fn calculate_gain_locations(
frame_offset: usize,
header: &FrameHeader,
out: &mut [GainLocation; MAX_GAIN_LOCATIONS],
) -> usize {
let side_info_start = frame_offset + header.side_info_offset();
let num_channels = header.channel_mode.channel_count();
let num_granules = header.granule_count();
let bits_before_granules = match (header.version, num_channels) {
(MpegVersion::Mpeg1, 1) => 18,
(MpegVersion::Mpeg1, _) => 20,
(_, 1) => 9,
(_, _) => 10,
};
let bits_per_granule_channel = match header.version {
MpegVersion::Mpeg1 => 59,
_ => 63,
};
let mut len = 0;
for gr in 0..num_granules {
for ch in 0..num_channels {
let granule_start_bit =
bits_before_granules + (gr * num_channels + ch) * bits_per_granule_channel;
let global_gain_bit = granule_start_bit + 21;
out[len] = GainLocation {
byte_offset: side_info_start + global_gain_bit / 8,
bit_offset: (global_gain_bit % 8) as u8,
};
len += 1;
}
}
len
}
pub(crate) fn read_bits_u8(data: &[u8], byte_offset: usize, bit_offset: u8) -> u8 {
if byte_offset >= data.len() {
return 0;
}
if bit_offset == 0 {
data[byte_offset]
} else if byte_offset + 1 < data.len() {
let high = data[byte_offset] << bit_offset;
let low = data[byte_offset + 1] >> (8 - bit_offset);
high | low
} else {
data[byte_offset] << bit_offset
}
}
pub(crate) fn write_bits_u8(data: &mut [u8], byte_offset: usize, bit_offset: u8, value: u8) {
if byte_offset >= data.len() {
return;
}
if bit_offset == 0 {
data[byte_offset] = value;
} else if byte_offset + 1 < data.len() {
let mask_high = 0xFFu8 << (8 - bit_offset);
let mask_low = 0xFFu8 >> bit_offset;
data[byte_offset] = (data[byte_offset] & mask_high) | (value >> bit_offset);
data[byte_offset + 1] = (data[byte_offset + 1] & mask_low) | (value << (8 - bit_offset));
} else {
let mask_high = 0xFFu8 << (8 - bit_offset);
data[byte_offset] = (data[byte_offset] & mask_high) | (value >> bit_offset);
}
}
pub(crate) fn read_gain_at(data: &[u8], loc: &GainLocation) -> u8 {
read_bits_u8(data, loc.byte_offset, loc.bit_offset)
}
pub(crate) fn write_gain_at(data: &mut [u8], loc: &GainLocation, value: u8) {
write_bits_u8(data, loc.byte_offset, loc.bit_offset, value)
}
pub(crate) fn skip_id3v2(data: &[u8]) -> usize {
if data.len() < 10 || &data[0..3] != b"ID3" {
return 0;
}
let size = ((data[6] as usize & 0x7F) << 21)
| ((data[7] as usize & 0x7F) << 14)
| ((data[8] as usize & 0x7F) << 7)
| (data[9] as usize & 0x7F);
10 + size
}
pub(crate) fn read_u32_le(data: &[u8]) -> u32 {
u32::from_le_bytes([data[0], data[1], data[2], data[3]])
}
pub(crate) fn find_audio_end(data: &[u8]) -> usize {
let mut audio_end = data.len();
if audio_end >= 128 && &data[audio_end - 128..audio_end - 125] == b"TAG" {
audio_end -= 128;
}
if audio_end >= 32 && &data[audio_end - 32..audio_end - 24] == APE_PREAMBLE {
let footer_start = audio_end - 32;
let tag_size = read_u32_le(&data[footer_start + 12..]) as usize;
let flags = read_u32_le(&data[footer_start + 20..]);
let has_header = (flags & APE_FLAG_HEADER_PRESENT) != 0;
let header_size = if has_header { 32 } else { 0 };
if footer_start + 32 >= tag_size + header_size {
audio_end = footer_start + 32 - tag_size - header_size;
}
}
audio_end
}
pub(crate) fn is_xing_frame(data: &[u8], frame_offset: usize, header: &FrameHeader) -> bool {
let side_info_len = match (header.version, header.channel_mode) {
(MpegVersion::Mpeg1, ChannelMode::Mono) => 17,
(MpegVersion::Mpeg1, _) => 32,
(_, ChannelMode::Mono) => 9,
(_, _) => 17,
};
let xing_offset = frame_offset + header.side_info_offset() + side_info_len;
if xing_offset + 4 > data.len() {
return false;
}
let marker = &data[xing_offset..xing_offset + 4];
marker == b"Xing" || marker == b"Info"
}
fn next_frame(
data: &[u8],
mut pos: usize,
audio_end: usize,
reference: Option<(MpegVersion, u32)>,
) -> Option<(usize, FrameHeader, usize)> {
while pos + 4 <= audio_end {
let header = match parse_header(&data[pos..]) {
Some(h) => h,
None => {
pos += 1;
continue;
}
};
let next_pos = pos + header.frame_size;
let valid_frame = if next_pos + 2 <= audio_end {
data[next_pos] == 0xFF && (data[next_pos + 1] & 0xE0) == 0xE0
} else {
next_pos <= audio_end
};
if !valid_frame {
pos += 1;
continue;
}
if is_xing_frame(data, pos, &header) {
pos = next_pos;
continue;
}
if let Some((ver, sr)) = reference {
if header.version != ver || header.sample_rate != sr {
pos += 1;
continue;
}
}
return Some((pos, header, next_pos));
}
None
}
pub(crate) fn iterate_frames<F>(data: &[u8], mut callback: F) -> Result<usize>
where
F: FnMut(usize, &FrameHeader, &[GainLocation]),
{
let audio_end = find_audio_end(data);
let mut pos = skip_id3v2(data);
let mut frame_count = 0;
let mut reference = None;
let mut locations = [GainLocation {
byte_offset: 0,
bit_offset: 0,
}; MAX_GAIN_LOCATIONS];
while let Some((frame_pos, header, next_pos)) = next_frame(data, pos, audio_end, reference) {
reference.get_or_insert((header.version, header.sample_rate));
let len = calculate_gain_locations(frame_pos, &header, &mut locations);
callback(frame_pos, &header, &locations[..len]);
frame_count += 1;
pos = next_pos;
}
Ok(frame_count)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum GainMode {
Saturating,
Wrapping,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SaturationStats {
pub frames: usize,
pub saturated_low: usize,
pub saturated_high: usize,
pub min_gain: u8,
pub max_gain: u8,
}
impl Default for SaturationStats {
fn default() -> Self {
Self {
frames: 0,
saturated_low: 0,
saturated_high: 0,
min_gain: 255,
max_gain: 0,
}
}
}
impl SaturationStats {
fn tally(&mut self, current: u8, steps: i32) {
let target = current as i32 + steps.clamp(-255, 255);
if target > 255 {
self.saturated_high += 1;
} else if target < 0 {
self.saturated_low += 1;
}
}
}
pub(crate) fn adjust_gain_value(current: u8, steps: i32, mode: GainMode) -> u8 {
match mode {
GainMode::Saturating => {
let steps = steps.clamp(-255, 255);
if steps > 0 {
current.saturating_add(steps as u8)
} else {
current.saturating_sub((-steps) as u8)
}
}
GainMode::Wrapping => ((current as i32 + steps.rem_euclid(256)) % 256) as u8,
}
}
pub(crate) fn apply_gain_to_data(
data: &mut [u8],
gain_steps: i32,
mode: GainMode,
channel_index: Option<usize>,
) -> SaturationStats {
let audio_end = find_audio_end(data);
let mut pos = skip_id3v2(data);
let mut stats = SaturationStats::default();
let mut reference = None;
let mut locations = [GainLocation {
byte_offset: 0,
bit_offset: 0,
}; MAX_GAIN_LOCATIONS];
while let Some((frame_pos, header, next_pos)) = next_frame(data, pos, audio_end, reference) {
reference.get_or_insert((header.version, header.sample_rate));
let len = calculate_gain_locations(frame_pos, &header, &mut locations);
match channel_index {
None => {
for loc in &locations[..len] {
let current_gain = read_gain_at(data, loc);
let new_gain = adjust_gain_value(current_gain, gain_steps, mode);
if mode == GainMode::Saturating {
stats.tally(current_gain, gain_steps);
}
stats.min_gain = stats.min_gain.min(new_gain);
stats.max_gain = stats.max_gain.max(new_gain);
write_gain_at(data, loc, new_gain);
}
}
Some(ch) => {
let num_channels = header.channel_mode.channel_count();
for gr in 0..header.granule_count() {
let loc_index = gr * num_channels + ch;
if loc_index < len {
let loc = &locations[loc_index];
let current_gain = read_gain_at(data, loc);
let new_gain =
adjust_gain_value(current_gain, gain_steps, GainMode::Saturating);
stats.tally(current_gain, gain_steps);
stats.min_gain = stats.min_gain.min(new_gain);
stats.max_gain = stats.max_gain.max(new_gain);
write_gain_at(data, loc, new_gain);
}
}
}
}
stats.frames += 1;
pos = next_pos;
}
stats
}
pub(crate) fn scan_gain_range(data: &[u8]) -> Result<(u8, u8)> {
let mut min_gain = 255u8;
let mut max_gain = 0u8;
let frame_count = iterate_frames(data, |_pos, _header, locations| {
for loc in locations {
let gain = read_gain_at(data, loc);
min_gain = min_gain.min(gain);
max_gain = max_gain.max(gain);
}
})?;
if frame_count == 0 {
return Err(Error::NoMp3Frames);
}
Ok((min_gain, max_gain))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_valid_header() {
let header = [0xFF, 0xFB, 0x90, 0x00];
let parsed = parse_header(&header);
assert!(parsed.is_some());
let h = parsed.unwrap();
assert_eq!(h.version, MpegVersion::Mpeg1);
assert_eq!(h.bitrate_kbps, 128);
assert_eq!(h.sample_rate, 44100);
}
#[test]
fn test_parse_invalid_header() {
assert!(parse_header(&[0x00, 0x00, 0x00, 0x00]).is_none());
assert!(parse_header(&[0xFF, 0xFF, 0x90, 0x00]).is_none());
}
#[test]
fn test_bit_operations() {
let mut data = vec![0xAB, 0xCD, 0xEF, 0x12, 0x34];
let loc_aligned = GainLocation {
byte_offset: 1,
bit_offset: 0,
};
assert_eq!(read_gain_at(&data, &loc_aligned), 0xCD);
let loc_unaligned = GainLocation {
byte_offset: 1,
bit_offset: 4,
};
assert_eq!(read_gain_at(&data, &loc_unaligned), 0xDE);
write_gain_at(&mut data, &loc_aligned, 0x42);
assert_eq!(data[1], 0x42);
data = vec![0xAB, 0xCD, 0xEF, 0x12, 0x34];
write_gain_at(&mut data, &loc_unaligned, 0x99);
assert_eq!(data[1], 0xC9);
assert_eq!(data[2], 0x9F);
}
#[test]
fn test_saturation_tally() {
let mut s = SaturationStats::default();
s.tally(200, 100); s.tally(250, 50); s.tally(10, -50); s.tally(100, 10); s.tally(255, 0); s.tally(0, 0); assert_eq!(s.saturated_high, 2);
assert_eq!(s.saturated_low, 1);
}
#[test]
fn test_skip_id3v2() {
let data_no_tag = vec![0xFF, 0xFB, 0x90, 0x00];
assert_eq!(skip_id3v2(&data_no_tag), 0);
let data_with_tag = vec![b'I', b'D', b'3', 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00];
assert_eq!(skip_id3v2(&data_with_tag), 10);
}
#[test]
fn test_is_xing_frame() {
let mut data = vec![0u8; 100];
data[0] = 0xFF;
data[1] = 0xFB;
data[2] = 0x90;
data[3] = 0x00;
data[36] = b'X';
data[37] = b'i';
data[38] = b'n';
data[39] = b'g';
let header = parse_header(&data).unwrap();
assert!(is_xing_frame(&data, 0, &header));
data[36] = b'I';
data[37] = b'n';
data[38] = b'f';
data[39] = b'o';
assert!(is_xing_frame(&data, 0, &header));
data[36] = 0x00;
data[37] = 0x00;
data[38] = 0x00;
data[39] = 0x00;
assert!(!is_xing_frame(&data, 0, &header));
}
fn make_frame(sr_idx: u8, gg: u8) -> Vec<u8> {
let sample_rate = SAMPLE_RATE_TABLE[0][sr_idx as usize] as usize;
let frame_size = (1152 * 128 * 125) / sample_rate;
let mut frame = vec![0u8; frame_size];
frame[0] = 0xFF;
frame[1] = 0xFB; frame[2] = (9 << 4) | (sr_idx << 2); frame[3] = 0x00;
let header = parse_header(&frame).unwrap();
let mut locs = [GainLocation {
byte_offset: 0,
bit_offset: 0,
}; MAX_GAIN_LOCATIONS];
let n = calculate_gain_locations(0, &header, &mut locs);
for loc in &locs[..n] {
write_gain_at(&mut frame, loc, gg);
}
frame
}
#[test]
fn test_scan_gain_range_skips_mismatched_sample_rate() {
let mut data = Vec::new();
for _ in 0..3 {
data.extend_from_slice(&make_frame(0, 150));
}
data.extend_from_slice(&make_frame(1, 243)); for _ in 0..2 {
data.extend_from_slice(&make_frame(0, 150));
}
assert_eq!(scan_gain_range(&data).unwrap(), (150, 150));
let mut ok = Vec::new();
for _ in 0..3 {
ok.extend_from_slice(&make_frame(0, 150));
}
ok.extend_from_slice(&make_frame(0, 243));
for _ in 0..2 {
ok.extend_from_slice(&make_frame(0, 150));
}
assert_eq!(scan_gain_range(&ok).unwrap(), (150, 243));
}
}