1use std::fs::File;
12use std::io::{self, Read, Seek, SeekFrom};
13use std::path::Path;
14
15#[derive(Debug, thiserror::Error)]
18pub enum MpegError {
19 #[error("can't sync to MPEG frame")]
20 HeaderNotFound,
21 #[error("{0}")]
24 Mutagen(String),
25}
26
27impl MpegError {
28 pub fn python_name(&self) -> &'static str {
30 match self {
31 MpegError::HeaderNotFound => "HeaderNotFoundError",
32 MpegError::Mutagen(_) => "MutagenError",
33 }
34 }
35}
36
37impl From<io::Error> for MpegError {
38 fn from(e: io::Error) -> Self {
39 MpegError::Mutagen(e.to_string())
40 }
41}
42
43#[derive(Debug, Clone, PartialEq)]
45pub struct MpegInfo {
46 pub length: f64,
48 pub bitrate: u64,
50 pub sample_rate: u32,
51 pub channels: u16,
52 pub sketchy: bool,
54}
55
56fn bitrate_table(version: Version, layer: u8) -> &'static [u32; 15] {
58 const V1L1: [u32; 15] = [
59 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448,
60 ];
61 const V1L2: [u32; 15] = [
62 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384,
63 ];
64 const V1L3: [u32; 15] = [
65 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320,
66 ];
67 const V2L1: [u32; 15] = [
68 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256,
69 ];
70 const V2L23: [u32; 15] = [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160];
71 match (version, layer) {
72 (Version::V1, 1) => &V1L1,
73 (Version::V1, 2) => &V1L2,
74 (Version::V1, _) => &V1L3,
75 (_, 1) => &V2L1,
76 _ => &V2L23,
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq)]
81enum Version {
82 V1,
83 V2,
84 V25,
85}
86
87fn sample_rates(version: Version) -> [u32; 3] {
88 match version {
89 Version::V1 => [44100, 48000, 32000],
90 Version::V2 => [22050, 24000, 16000],
91 Version::V25 => [11025, 12000, 8000],
92 }
93}
94
95struct Frame {
98 frame_offset: u64,
99 next_offset: u64,
101 bitrate: u64,
102 sample_rate: u32,
103 channels: u16,
104 sketchy: bool,
105 length: Option<f64>,
106}
107
108fn read_exact_at<R: Read + Seek>(f: &mut R, pos: u64, n: usize) -> io::Result<Option<Vec<u8>>> {
109 f.seek(SeekFrom::Start(pos))?;
110 let mut buf = vec![0u8; n];
111 let mut got = 0;
112 while got < n {
113 let k = f.read(&mut buf[got..])?;
114 if k == 0 {
115 break;
116 }
117 got += k;
118 }
119 Ok(if got == n { Some(buf) } else { None })
120}
121
122fn u32be(b: &[u8]) -> u32 {
123 u32::from_be_bytes([b[0], b[1], b[2], b[3]])
124}
125fn u16be(b: &[u8]) -> u16 {
126 u16::from_be_bytes([b[0], b[1]])
127}
128
129fn skip_id3<R: Read + Seek>(f: &mut R, mut pos: u64) -> io::Result<u64> {
131 loop {
132 let Some(h) = read_exact_at(f, pos, 10)? else {
133 return Ok(pos);
134 };
135 let insize = h[6..10]
137 .iter()
138 .fold(0u64, |acc, b| (acc << 7) | u64::from(b & 0x7f));
139 if &h[0..3] == b"ID3" && insize > 0 {
140 pos += 10 + insize;
141 } else {
142 return Ok(pos);
143 }
144 }
145}
146
147fn parse_frame<R: Read + Seek>(f: &mut R, pos: u64) -> io::Result<Option<Frame>> {
149 let Some(h) = read_exact_at(f, pos, 4)? else {
150 return Ok(None); };
152 let bits = u32be(&h);
153 if bits >> 21 != 0x7ff {
154 return Ok(None); }
156 let version = (bits >> 19) & 0x3;
157 let layer_bits = (bits >> 17) & 0x3;
158 let bitrate_idx = (bits >> 12) & 0xf;
159 let sr_idx = (bits >> 10) & 0x3;
160 let padding = (bits >> 9) & 0x1;
161 let mode = (bits >> 6) & 0x3;
162
163 if version == 1 || layer_bits == 0 || sr_idx == 0x3 || bitrate_idx == 0xf || bitrate_idx == 0 {
165 return Ok(None);
166 }
167 let version = match version {
168 0 => Version::V25,
169 2 => Version::V2,
170 _ => Version::V1,
171 };
172 let layer = (4 - layer_bits) as u8;
173 let channels: u16 = if mode == 3 { 1 } else { 2 };
174 let bitrate = u64::from(bitrate_table(version, layer)[bitrate_idx as usize]) * 1000;
175 let sample_rate = sample_rates(version)[sr_idx as usize];
176
177 let (frame_size, slot): (u64, u64) = if layer == 1 {
178 (384, 4)
179 } else if version != Version::V1 && layer == 3 {
180 (576, 1)
181 } else {
182 (1152, 1)
183 };
184 let frame_length =
185 ((frame_size / 8 * bitrate) / u64::from(sample_rate) + u64::from(padding)) * slot;
186
187 let mut frame = Frame {
188 frame_offset: pos,
189 next_offset: pos + frame_length,
190 bitrate,
191 sample_rate,
192 channels,
193 sketchy: true,
194 length: None,
195 };
196 if layer == 3 {
197 parse_vbr_header(f, &mut frame, version, mode, frame_size, frame_length)?;
198 }
199 Ok(Some(frame))
200}
201
202fn parse_vbr_header<R: Read + Seek>(
205 f: &mut R,
206 frame: &mut Frame,
207 version: Version,
208 mode: u32,
209 frame_size: u64,
210 frame_length: u64,
211) -> io::Result<()> {
212 let xing_offset: u64 = match (version, mode) {
213 (Version::V1, m) if m != 3 => 36,
214 (Version::V1, _) => 21,
215 (_, m) if m != 3 => 21,
216 _ => 13,
217 };
218 if let Some(xing) = parse_xing(f, frame.frame_offset + xing_offset)? {
219 frame.sketchy = false;
220 if let Some(frames) = xing.frames {
221 let mut samples = frame_size as i64 * i64::from(frames);
222 if let Some(bytes) = xing.bytes {
223 if samples > 0 {
224 let audio_bytes = (i64::from(bytes) - frame_length as i64).max(0);
227 let v =
228 (audio_bytes as f64 * 8.0 * f64::from(frame.sample_rate)) / samples as f64;
229 frame.bitrate = intround(v);
230 }
231 }
232 if let Some((delay, padding)) = xing.lame_delay_padding {
233 samples -= i64::from(delay);
234 samples -= i64::from(padding);
235 }
236 if samples < 0 {
237 samples = 0;
240 }
241 frame.length = Some(samples as f64 / f64::from(frame.sample_rate));
242 }
243 return Ok(());
244 }
245
246 if let Some((vbri_bytes, vbri_frames)) = parse_vbri(f, frame.frame_offset + 36)? {
247 frame.sketchy = false;
248 let length = (frame_size * u64::from(vbri_frames)) as f64 / f64::from(frame.sample_rate);
249 frame.length = Some(length);
250 if length != 0.0 {
251 frame.bitrate = ((f64::from(vbri_bytes) * 8.0) / length) as u64;
252 }
253 }
254 Ok(())
255}
256
257fn intround(v: f64) -> u64 {
259 let r = v.round_ties_even();
260 if r < 0.0 {
261 0
262 } else {
263 r as u64
264 }
265}
266
267struct Xing {
268 frames: Option<u32>,
269 bytes: Option<u32>,
270 lame_delay_padding: Option<(u32, u32)>,
272}
273
274fn parse_xing<R: Read + Seek>(f: &mut R, pos: u64) -> io::Result<Option<Xing>> {
275 let Some(head) = read_exact_at(f, pos, 8)? else {
276 return Ok(None);
277 };
278 if &head[0..4] != b"Xing" && &head[0..4] != b"Info" {
279 return Ok(None);
280 }
281 let flags = u32be(&head[4..8]);
282 let mut cur = pos + 8;
283 let mut take = |f: &mut R, n: usize| -> io::Result<Option<Vec<u8>>> {
284 let r = read_exact_at(f, cur, n)?;
285 cur += n as u64;
286 Ok(r)
287 };
288 let mut xing = Xing {
289 frames: None,
290 bytes: None,
291 lame_delay_padding: None,
292 };
293 if flags & 0x1 != 0 {
294 let Some(b) = take(f, 4)? else {
295 return Ok(None); };
297 xing.frames = Some(u32be(&b));
298 }
299 if flags & 0x2 != 0 {
300 let Some(b) = take(f, 4)? else {
301 return Ok(None);
302 };
303 xing.bytes = Some(u32be(&b));
304 }
305 if flags & 0x4 != 0 && take(f, 100)?.is_none() {
306 return Ok(None);
307 }
308 if flags & 0x8 != 0 && take(f, 4)?.is_none() {
309 return Ok(None);
310 }
311 xing.lame_delay_padding = parse_lame(f, cur)?;
315 Ok(Some(xing))
316}
317
318fn parse_lame<R: Read + Seek>(f: &mut R, pos: u64) -> io::Result<Option<(u32, u32)>> {
322 let Some(data) = read_exact_at(f, pos, 20)? else {
323 return Ok(None);
324 };
325 if !(data.starts_with(b"LAME") || data.starts_with(b"L3.99")) {
326 return Ok(None);
327 }
328 let mut i = 0;
330 while i < data.len() && matches!(data[i], b'E' | b'M' | b'A' | b'L') {
331 i += 1;
332 }
333 let major = data.get(i).copied();
334 i += 1;
335 while i < data.len() && data[i] == b'.' {
337 i += 1;
338 }
339 let minor_start = i;
340 while i < data.len() && data[i].is_ascii_digit() {
341 i += 1;
342 }
343 let minor_digits = &data[minor_start..i];
344 let major = match major {
345 Some(c) if c.is_ascii_digit() => u32::from(c - b'0'),
346 _ => return Ok(None),
347 };
348 if minor_digits.is_empty() {
349 return Ok(None);
350 }
351 let minor: u32 = std::str::from_utf8(minor_digits)
352 .ok()
353 .and_then(|s| s.parse().ok())
354 .unwrap_or(0);
355 let rest = &data[i.min(data.len())..];
356
357 let paren = rest.len() >= 11 && data.get(9) == Some(&b'(');
359 if (major, minor) < (3, 90) || ((major, minor) == (3, 90) && paren) {
360 return Ok(None);
361 }
362 if rest.len() < 11 {
363 return Ok(None); }
365 let Some(payload) = read_exact_at(f, pos + 9, 27)? else {
367 return Ok(None); };
369 if payload[0] >> 4 != 0 {
370 return Ok(None); }
372 let delay = (u32::from(payload[12]) << 4) | (u32::from(payload[13]) >> 4);
373 let padding = ((u32::from(payload[13]) & 0xf) << 8) | u32::from(payload[14]);
374 Ok(Some((delay, padding)))
375}
376
377fn parse_vbri<R: Read + Seek>(f: &mut R, pos: u64) -> io::Result<Option<(u32, u32)>> {
379 let Some(d) = read_exact_at(f, pos, 26)? else {
380 return Ok(None);
381 };
382 if !d.starts_with(b"VBRI") || u16be(&d[4..6]) != 1 {
383 return Ok(None);
384 }
385 let bytes = u32be(&d[10..14]);
386 let frames = u32be(&d[14..18]);
387 let toc_entries = u64::from(u16be(&d[18..20]));
388 let toc_entry_size = u16be(&d[22..24]);
389 if toc_entry_size != 2 && toc_entry_size != 4 {
390 return Ok(None);
391 }
392 let toc_size = (toc_entries * u64::from(toc_entry_size)) as usize;
393 if read_exact_at(f, pos + 26, toc_size)?.is_none() {
394 return Ok(None); }
396 Ok(Some((bytes, frames)))
397}
398
399pub fn info_from<R: Read + Seek>(f: &mut R) -> Result<MpegInfo, MpegError> {
402 let start = skip_id3(f, 0)?;
403
404 const MAX_READ: usize = 1024 * 1024;
406 const MAX_SYNCS: usize = 1500;
407 const ENOUGH_FRAMES: usize = 4;
408 const MIN_FRAMES: usize = 2;
409
410 f.seek(SeekFrom::Start(start))?;
411 let mut region = Vec::with_capacity(MAX_READ.min(1 << 16));
412 f.by_ref().take(MAX_READ as u64).read_to_end(&mut region)?;
413
414 let mut sketchy = true;
415 let mut first_frame: Option<Frame> = None;
416 let mut syncs_left = MAX_SYNCS;
417 let mut frames: Vec<Frame> = Vec::new();
418
419 let mut i = 0;
420 while i + 1 < region.len() {
421 if !(region[i] == 0xff && region[i + 1] & 0xe0 == 0xe0) {
422 i += 1;
423 continue;
424 }
425 let sync_pos = start + i as u64;
426 i += 1;
427
428 syncs_left -= 1;
429 if syncs_left == 0 {
430 break;
431 }
432
433 let mut next = sync_pos;
434 for _ in 0..ENOUGH_FRAMES {
435 let Some(fr) = parse_frame(f, next)? else {
436 break;
437 };
438 next = fr.next_offset;
439 let stop = !fr.sketchy;
440 frames.push(fr);
441 if stop {
442 break;
443 }
444 }
445
446 if frames.len() >= MIN_FRAMES && first_frame.is_none() {
447 first_frame = Some(clone_frame(&frames[0]));
448 }
449 if frames.last().is_some_and(|fr| !fr.sketchy) {
450 first_frame = frames.pop();
451 sketchy = false;
452 break;
453 }
454 if frames.len() >= ENOUGH_FRAMES {
455 first_frame = Some(frames.swap_remove(0));
456 sketchy = false;
457 break;
458 }
459 frames.clear();
460 }
461
462 let Some(first) = first_frame else {
463 return Err(MpegError::HeaderNotFound);
464 };
465
466 let length = match first.length {
467 Some(l) => l,
468 None => {
469 let end = f.seek(SeekFrom::End(0))?;
471 let content_size = end.saturating_sub(first.frame_offset);
472 8.0 * content_size as f64 / first.bitrate as f64
473 }
474 };
475 Ok(MpegInfo {
476 length,
477 bitrate: first.bitrate,
478 sample_rate: first.sample_rate,
479 channels: first.channels,
480 sketchy,
481 })
482}
483
484fn clone_frame(fr: &Frame) -> Frame {
485 Frame {
486 frame_offset: fr.frame_offset,
487 next_offset: fr.next_offset,
488 bitrate: fr.bitrate,
489 sample_rate: fr.sample_rate,
490 channels: fr.channels,
491 sketchy: fr.sketchy,
492 length: fr.length,
493 }
494}
495
496pub fn info(path: &Path) -> Result<MpegInfo, MpegError> {
498 let mut f = File::open(path)?;
499 info_from(&mut f)
500}
501
502pub fn duration_ms(path: &Path) -> Result<i64, MpegError> {
504 Ok((info(path)?.length * 1000.0) as i64)
505}
506
507#[cfg(test)]
508mod tests {
509 use super::*;
510 use std::process::Command;
511
512 fn ffmpeg_available() -> bool {
513 crate::ffmpeg::available()
514 }
515
516 fn gen(dir: &Path, name: &str, rate: u32, ch: u32, extra: &[&str]) -> std::path::PathBuf {
517 let out = dir.join(name);
518 let filt = format!("sine=frequency=440:duration=2.3:sample_rate={rate}");
519 let st = Command::new("ffmpeg")
520 .args(["-v", "quiet", "-y", "-f", "lavfi", "-i", &filt, "-ac"])
521 .arg(ch.to_string())
522 .args(extra)
523 .arg(&out)
524 .status()
525 .unwrap();
526 assert!(st.success());
527 out
528 }
529
530 #[test]
533 #[allow(clippy::type_complexity)]
534 fn matches_mutagen_on_lame_encoded_tones() {
535 if !ffmpeg_available() {
536 return;
537 }
538 let tmp = tempfile::tempdir().unwrap();
539 let d = tmp.path();
540 let cases: [(&str, u32, u32, &[&str], f64, u64, bool); 5] = [
541 (
542 "stereo44.mp3",
543 44100,
544 2,
545 &[],
546 2.3510204081632655,
547 127999,
548 false,
549 ),
550 (
551 "mono44.mp3",
552 44100,
553 1,
554 &[],
555 2.3510204081632655,
556 63999,
557 false,
558 ),
559 (
560 "mono22.mp3",
561 22050,
562 1,
563 &[],
564 2.3771428571428572,
565 32001,
566 false,
567 ),
568 (
569 "stereo11.mp3",
570 11025,
571 2,
572 &[],
573 2.455510204081633,
574 32000,
575 false,
576 ),
577 (
578 "noxing.mp3",
579 44100,
580 2,
581 &["-write_xing", "0"],
582 2.351,
583 128000,
584 false,
585 ),
586 ];
587 for (name, rate, ch, extra, len, br, sk) in cases {
588 let p = gen(d, name, rate, ch, extra);
589 let i = info(&p).unwrap();
590 assert_eq!(i.length, len, "{name} length");
591 assert_eq!(i.bitrate, br, "{name} bitrate");
592 assert_eq!(i.sketchy, sk, "{name} sketchy");
593 assert_eq!(i.channels, ch as u16, "{name} channels");
594 assert_eq!(i.sample_rate, rate, "{name} rate");
595 }
596 }
597
598 #[test]
599 fn non_audio_is_header_not_found() {
600 let tmp = tempfile::tempdir().unwrap();
601 for (name, bytes) in [
602 ("empty.mp3", &b""[..]),
603 ("fake.mp3", b"ID3\x03\x00door"),
604 ("text.mp3", b"hello\n"),
605 ] {
606 let p = tmp.path().join(name);
607 std::fs::write(&p, bytes).unwrap();
608 let e = info(&p).unwrap_err();
609 assert_eq!(e.python_name(), "HeaderNotFoundError", "{name}");
610 assert_eq!(e.to_string(), "can't sync to MPEG frame");
611 }
612 let e = info(&tmp.path().join("missing.mp3")).unwrap_err();
613 assert_eq!(e.python_name(), "MutagenError");
614 let e = info(tmp.path()).unwrap_err();
615 assert_eq!(e.python_name(), "MutagenError");
616 }
617
618 #[test]
619 fn duration_ms_truncates_like_int() {
620 if !ffmpeg_available() {
621 return;
622 }
623 let tmp = tempfile::tempdir().unwrap();
624 let p = gen(tmp.path(), "s.mp3", 44100, 2, &[]);
625 assert_eq!(duration_ms(&p).unwrap(), 2351);
626 }
627
628 #[test]
629 fn intround_is_half_to_even() {
630 assert_eq!(intround(2.5), 2);
631 assert_eq!(intround(3.5), 4);
632 assert_eq!(intround(127998.7), 127999);
633 }
634
635 #[test]
639 #[ignore]
640 fn probe_library() {
641 let Ok(dir) = std::env::var("XIL_MPEG_PROBE_DIR") else {
642 return;
643 };
644 let out = std::env::var("XIL_MPEG_PROBE_OUT").unwrap();
645 let mut lines = Vec::new();
646 fn walk(d: &Path, acc: &mut Vec<std::path::PathBuf>) {
647 for e in std::fs::read_dir(d).unwrap().flatten() {
648 let p = e.path();
649 if p.is_dir() {
650 walk(&p, acc);
651 } else if p.extension().is_some_and(|x| x.eq_ignore_ascii_case("mp3")) {
652 acc.push(p);
653 }
654 }
655 }
656 let mut files = Vec::new();
657 walk(Path::new(&dir), &mut files);
658 files.sort();
659 for p in files {
660 let line = match info(&p) {
661 Ok(i) => format!("{}\t{:?}\t{}", p.display(), i.length, i.bitrate),
662 Err(e) => format!("{}\tERR\t{}", p.display(), e.python_name()),
663 };
664 lines.push(line);
665 }
666 std::fs::write(out, lines.join("\n") + "\n").unwrap();
667 }
668}