From 9ba4eed1ea94bbcfb72b3a7a3b85356e68d173f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Mon, 8 Sep 2025 20:29:23 +0900 Subject: [PATCH] fix: reduce mp3 false-positive --- um_audio/src/__fixtures__/ffmpeg_silent.mp3 | Bin 0 -> 8567 bytes um_audio/src/__fixtures__/junk.bin | Bin 0 -> 4096 bytes .../src/__fixtures__/mp3_id3v2_with_junk.bin | Bin 176 -> 784 bytes um_audio/src/__fixtures__/mp3_with_id3v2.bin | Bin 64 -> 0 bytes .../src/__fixtures__/mp3_with_id3v2_x3.bin | Bin 128 -> 733 bytes um_audio/src/aac.rs | 9 ++ um_audio/src/lib.rs | 32 +++-- um_audio/src/mp3.rs | 122 ++++++++++++++++++ um_audio/src/sync_frame.rs | 27 ---- 9 files changed, 146 insertions(+), 44 deletions(-) create mode 100644 um_audio/src/__fixtures__/ffmpeg_silent.mp3 create mode 100644 um_audio/src/__fixtures__/junk.bin delete mode 100644 um_audio/src/__fixtures__/mp3_with_id3v2.bin create mode 100644 um_audio/src/aac.rs create mode 100644 um_audio/src/mp3.rs delete mode 100644 um_audio/src/sync_frame.rs diff --git a/um_audio/src/__fixtures__/ffmpeg_silent.mp3 b/um_audio/src/__fixtures__/ffmpeg_silent.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..ba822287cd0dd0ce669f4e6c7e5a711ce2564fd7 GIT binary patch literal 8567 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 delta 23 ecmbQhwt;cN0)gKTQW$(3eO--1T!TYg976zWGzc;P diff --git a/um_audio/src/__fixtures__/mp3_with_id3v2.bin b/um_audio/src/__fixtures__/mp3_with_id3v2.bin deleted file mode 100644 index d3b47e6b7ced966905371a00451f4586ffadf819..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64 wcmeZtF=k-^0p*b3U{?kP27U$xW}n2eG*b&bb2B|d0|SsG6#xJI1j>R_04{_GX8-^I diff --git a/um_audio/src/__fixtures__/mp3_with_id3v2_x3.bin b/um_audio/src/__fixtures__/mp3_with_id3v2_x3.bin index dc4e1ba950c636d60fc9ea6367e5f1e419a5fa0c..6f9ccd32fd550325bd507f0c407177ada2c2fec8 100644 GIT binary patch literal 733 zcmeZtF=k-^0p*b3U{?kP27U$xW}n2eG*b&bb2B|d0|SsG6vGvZMMg$OGB7akKouJr z=^2~o86e~%A|fJ?xw*5mv%i1p)TwjkELyZ^ z#fo+7)@|Lod-v{x2ag{=e(v0rD_3sadhp=E^XG5hzWx09=g*)2|ASoPlUSB)W~66m z2;#u73Tq97saVc>(Cj{hjUfO34oG3(|G>bI$-uzC!N9<)z`(%Bz`(eKfq_ARfx)DK ufq})x(bqLJG!#S`>lqptDrh97mLw_|YlaR!@c(yEN*DqEz+62PJOltAB*^su delta 24 Scmcc1+Q2v=U+@V78UO%9D+5>n diff --git a/um_audio/src/aac.rs b/um_audio/src/aac.rs new file mode 100644 index 0000000..0ff14a6 --- /dev/null +++ b/um_audio/src/aac.rs @@ -0,0 +1,9 @@ +pub const SYNC_FRAME_TEST_SIZE: usize = 4096; + +pub fn is_aac(magic: u32) -> bool { + // Frame sync should have the first 12 bits set to 1. + const AAC_AND_MASK: u32 = 0b1111_1111_1111_0110u32 << 16; + const AAC_EXPECTED: u32 = 0b1111_1111_1111_0000u32 << 16; + + (magic & AAC_AND_MASK) == AAC_EXPECTED +} diff --git a/um_audio/src/lib.rs b/um_audio/src/lib.rs index 05f0454..8c9a945 100644 --- a/um_audio/src/lib.rs +++ b/um_audio/src/lib.rs @@ -1,10 +1,11 @@ -mod metadata; +mod aac; mod audio_type; -mod sync_frame; +mod metadata; +mod mp3; -use crate::sync_frame::SYNC_FRAME_TEST_SIZE; +use crate::aac::SYNC_FRAME_TEST_SIZE; +use aac::is_aac; pub use audio_type::{AudioError, AudioType}; -use sync_frame::{is_aac, is_mp3}; const MAGIC_FLAC: [u8; 4] = *b"fLaC"; const MAGIC_OGG: [u8; 4] = *b"OggS"; @@ -35,7 +36,7 @@ pub fn detect_audio_type(buffer: &[u8]) -> Result { let magic = u32::from_be_bytes(magic); if is_aac(magic) { return Ok(AudioType::AAC); - } else if is_mp3(magic) { + } else if mp3::is_mp3(buffer) { return Ok(AudioType::MP3); } @@ -61,17 +62,7 @@ pub fn detect_audio_type(buffer: &[u8]) -> Result { }; } - // brute force test for MP3 / AAC - for magic_window in buffer.windows(4).take(SYNC_FRAME_TEST_SIZE) { - let magic = u32::from_be_bytes(magic_window.try_into().unwrap()); - if is_mp3(magic) { - return Ok(AudioType::MP3); - } else if is_aac(magic) { - return Ok(AudioType::AAC); - } - } - - // Ask for more data to test for MP3 / AAC + // Ask for more data to test for MP3 if buffer.len() < SYNC_FRAME_TEST_SIZE { return Err(AudioError::NeedMoreHeader(offset + SYNC_FRAME_TEST_SIZE)); } @@ -85,7 +76,7 @@ mod tests { #[test] fn test_mp3() { - let mp3_data = include_bytes!("__fixtures__/mp3_with_id3v2.bin"); + let mp3_data = include_bytes!("__fixtures__/ffmpeg_silent.mp3"); let result = detect_audio_type(mp3_data).expect("failed to parse mp3"); assert_eq!(result, AudioType::MP3); } @@ -111,4 +102,11 @@ mod tests { let result = detect_audio_type(&mp3_data).expect("failed to parse mp3"); assert_eq!(result, AudioType::Unknown); } + + #[test] + fn test_mp3_invalid_2() { + let mp3_data = include_bytes!("__fixtures__/junk.bin"); + let result = detect_audio_type(mp3_data).expect("failed to parse mp3"); + assert_eq!(result, AudioType::Unknown); + } } diff --git a/um_audio/src/mp3.rs b/um_audio/src/mp3.rs new file mode 100644 index 0000000..8326c1c --- /dev/null +++ b/um_audio/src/mp3.rs @@ -0,0 +1,122 @@ +pub fn is_mp3(buf: &[u8]) -> bool { + scan_for_mp3(buf) >= 3 +} + +pub fn scan_for_mp3(buf: &[u8]) -> usize { + let n = buf.len(); + if n < 4 { + return 0; + } + + let mut cache = vec![0; n]; + + // Scan through buffer for a possible frame header + for i in 0..n - 4 { + let h = u32::from_be_bytes([buf[i], buf[i + 1], buf[i + 2], buf[i + 3]]); + if let Some(frame_size) = parse_mp3_header(h) { + cache[i] = i + frame_size; + } + } + + // find the longest chain of valid frames + let mut result = 0; + for i in 0..n - 4 { + let mut result_at_i = 0; + + let mut i = i; + while i < n && cache[i] != 0 { + result_at_i += 1; + i = cache[i]; + } + + result = result.max(result_at_i); + } + + result +} + +fn parse_mp3_header(h: u32) -> Option { + let sync = (h >> 21) & 0x7FF; + if sync != 0x7FF { + return None; + } + + let version_id = (h >> 19) & 0b11; + if version_id == 0b01 { + return None; // reserved + } + + let layer = (h >> 17) & 0b11; + if layer == 0b00 { + return None; // reserved + } + + let bitrate_idx = (h >> 12) & 0b1111; + if bitrate_idx == 0b0000 || bitrate_idx == 0b1111 { + return None; + } + + let sampling_idx = (h >> 10) & 0b11; + if sampling_idx == 0b11 { + return None; + } + + let padding = (h >> 9) & 0b1; + + // Lookup tables + let bitrate = bitrate_kbps(version_id, layer, bitrate_idx)? * 1000; + let sample_rate = sample_rate_hz(version_id, sampling_idx)?; + + let frame_len = match (version_id, layer) { + // Layer I + (_, 0b11) => ((12 * bitrate / sample_rate) + padding) * 4, + // Layer II or III + (0b11, _) => (144 * bitrate / sample_rate) + padding, // MPEG1 + (_, _) => (72 * bitrate / sample_rate) + padding, // MPEG2/2.5 + }; + + Some(frame_len as usize) +} + +fn bitrate_kbps(version: u32, layer: u32, idx: u32) -> Option { + if idx == 0 || idx == 15 || layer == 0 { + // invalid + return None; + } + + let table = match (version, layer) { + // MPEG Version 1 + (0b11, 0b11) => [ + 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, + ], + (0b11, 0b10) => [ + 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, + ], + (0b11, 0b01) => [ + 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, + ], + // MPEG Version 2 or 2.5 + // Layer I + (_, 0b11) => [ + 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, + ], + // Layer II + _ => [8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160], + }; + + Some(table[(idx - 1) as usize]) +} + +fn sample_rate_hz(version: u32, idx: u32) -> Option { + let table = match version { + // MPEG Version 1 + 0b11 => Some([44100, 48000, 32000]), + // MPEG Version 2 + 0b10 => Some([22050, 24000, 16000]), + // MPEG Version 2.5 + 0b00 => Some([11025, 12000, 8000]), + _ => None, + }?; + + Some(table[idx as usize]) +} diff --git a/um_audio/src/sync_frame.rs b/um_audio/src/sync_frame.rs deleted file mode 100644 index b717554..0000000 --- a/um_audio/src/sync_frame.rs +++ /dev/null @@ -1,27 +0,0 @@ -pub const SYNC_FRAME_TEST_SIZE: usize = 0xff; - -pub fn is_mp3(magic: u32) -> bool { - // Check for 11-bit sync word, followed by 2 bits of version, and 2 bits of layer. - // MPEG Version: MPEG Version 2 (ISO/IEC 13818-3) or MPEG Version 1 (ISO/IEC 11172-3) - const MP3_AND_MASK: u32 = 0b1111_1111_1111_0110u32 << 16; - const MP3_EXPECTED: u32 = 0b1111_1111_1111_0010u32 << 16; - - if (magic & MP3_AND_MASK) != MP3_EXPECTED { - return false; - } - - // Check for bitrate index and sampling rate frequency index. - let bitrate = ((magic >> 12) & 0b1111) as u8; - let sampling_rate = ((magic >> 10) & 0b11) as u8; - - // They should not be all 1s. - bitrate != 0b1111 && sampling_rate != 0b11 -} - -pub fn is_aac(magic: u32) -> bool { - // Frame sync should have the first 12 bits set to 1. - const AAC_AND_MASK: u32 = 0b1111_1111_1111_0110u32 << 16; - const AAC_EXPECTED: u32 = 0b1111_1111_1111_0000u32 << 16; - - (magic & AAC_AND_MASK) == AAC_EXPECTED -}