use super::cues::{self, Kind};
use super::pssi;
use super::section::AnlzFile;
use crate::collection::Cue;
use anyhow::{Context, Result};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FileKind {
Dat,
Ext,
TwoEx,
}
impl FileKind {
pub const ALL: [FileKind; 3] = [FileKind::Dat, FileKind::Ext, FileKind::TwoEx];
pub fn extension(self) -> &'static str {
match self {
FileKind::Dat => "DAT",
FileKind::Ext => "EXT",
FileKind::TwoEx => "2EX",
}
}
}
pub fn prepare(file: &mut AnlzFile, kind: FileKind, usb_path: &str, cues_xml: &[Cue], bpm: f64) {
file.set_path(usb_path);
match kind {
FileKind::Dat => cues::splice(file, Kind::Dat, cues_xml, bpm),
FileKind::Ext => {
cues::splice(file, Kind::Ext, cues_xml, bpm);
if let Some(s) = file.find_mut(b"PSSI") {
if pssi::is_plain(&s.bytes) {
pssi::toggle_mask(&mut s.bytes);
}
}
}
FileKind::TwoEx => {}
}
}
pub fn set_cbr_pvbr(file: &mut AnlzFile, audio_frames: u32) {
if let Some(s) = file.find_mut(b"PVBR") {
if s.bytes.len() == 1620 {
s.bytes[16..1616].fill(0);
s.bytes[1616..1620].copy_from_slice(&audio_frames.wrapping_mul(1152).to_be_bytes());
}
}
}
pub fn strip_pvb2(file: &mut AnlzFile) {
file.remove(b"PVB2");
}
#[derive(Debug, Clone, Copy, Default, PartialEq)]
pub struct Mp3Audio {
pub frames: u32,
pub bytes: u64,
pub sample_rate: u32,
}
impl Mp3Audio {
pub fn kbps(&self) -> Option<u32> {
if self.frames == 0 || self.sample_rate == 0 {
return None;
}
let secs = self.frames as f64 * 1152.0 / self.sample_rate as f64;
Some((self.bytes as f64 * 8.0 / secs / 1000.0).round() as u32)
}
}
pub fn mp3_audio(path: &Path) -> Result<Mp3Audio> {
let data = std::fs::read(path).with_context(|| format!("reading {}", path.display()))?;
let mut pos = 0usize;
if data.starts_with(b"ID3") && data.len() > 10 {
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);
pos = 10 + size;
}
const BITRATES: [u32; 16] = [
0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0,
];
const RATES: [u32; 4] = [44100, 48000, 32000, 0];
let mut audio = Mp3Audio::default();
let mut first = true;
while pos + 4 <= data.len() {
let h = &data[pos..pos + 4];
let sync = h[0] == 0xff && (h[1] & 0xe0) == 0xe0;
let mpeg1_layer3 = (h[1] & 0x18) == 0x18 && (h[1] & 0x06) == 0x02;
let bitrate = BITRATES[(h[2] >> 4) as usize];
let rate = RATES[((h[2] >> 2) & 3) as usize];
if !(sync && mpeg1_layer3) || bitrate == 0 || rate == 0 {
if audio.frames == 0 {
pos += 1; continue;
}
break;
}
let len = (144 * bitrate * 1000 / rate + ((h[2] >> 1) & 1) as u32) as usize;
if first {
first = false;
let body = &data[pos..(pos + len).min(data.len())];
if body.windows(4).any(|w| w == b"Xing" || w == b"Info") {
pos += len;
continue;
}
}
audio.frames += 1;
audio.bytes += len as u64;
audio.sample_rate = rate;
pos += len;
}
Ok(audio)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mp3_audio_excludes_tags_and_the_info_frame() {
let frame = |padding: bool| {
let mut f = vec![0u8; 1044 + padding as usize];
f[..4].copy_from_slice(&[0xff, 0xfb, 0xe0 | ((padding as u8) << 1), 0]);
f
};
let tag_size = 200_000usize; let mut data = b"ID3\x03\x00\x00".to_vec();
data.extend((0..4).rev().map(|i| ((tag_size >> (7 * i)) & 0x7f) as u8));
data.resize(10 + tag_size, 0);
let mut info = frame(false);
info[36..40].copy_from_slice(b"Info");
data.extend(info);
for i in 0..1000 {
data.extend(frame(i % 9 != 0));
}
let path = std::env::temp_dir().join(format!("baken-mp3-audio-{}.mp3", std::process::id()));
std::fs::write(&path, &data).unwrap();
let audio = mp3_audio(&path).unwrap();
std::fs::remove_file(&path).unwrap();
assert_eq!(audio.frames, 1000);
assert_eq!(audio.sample_rate, 44100);
assert_eq!(audio.kbps(), Some(320));
assert_eq!(Mp3Audio::default().kbps(), None);
}
#[test]
fn cbr_trailer() {
let mut f = AnlzFile {
header_tail: [0; 16],
sections: vec![super::super::section::section(b"PVBR", 0x10, &[7u8; 1608])],
};
set_cbr_pvbr(&mut f, 19698);
let s = f.find(b"PVBR").unwrap();
assert!(s.bytes[16..1616].iter().all(|&b| b == 0));
assert_eq!(&s.bytes[1616..], &22692096u32.to_be_bytes());
}
}