From 175da7f287a2370bfd3de0f54f7e9e7d7fa6d531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=B2=81=E6=A0=91=E4=BA=BA?= Date: Sun, 15 Sep 2024 00:07:46 +0100 Subject: [PATCH] feat: added audio type detector --- .idea/lib_um_crypto.iml | 1 + Cargo.lock | 9 +++ Cargo.toml | 2 +- um_audio/Cargo.toml | 8 +++ um_audio/src/lib.rs | 131 +++++++++++++++++++++++++++++++++++ um_audio/src/metadata.rs | 75 ++++++++++++++++++++ um_wasm/Cargo.toml | 1 + um_wasm/src/exports/audio.rs | 31 +++++++++ um_wasm/src/exports/mod.rs | 1 + 9 files changed, 258 insertions(+), 1 deletion(-) create mode 100644 um_audio/Cargo.toml create mode 100644 um_audio/src/lib.rs create mode 100644 um_audio/src/metadata.rs create mode 100644 um_wasm/src/exports/audio.rs diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml index 3a3b439..18a5311 100644 --- a/.idea/lib_um_crypto.iml +++ b/.idea/lib_um_crypto.iml @@ -9,6 +9,7 @@ + diff --git a/Cargo.lock b/Cargo.lock index 36bf3f7..5ad2119 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -427,6 +427,14 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "um_audio" +version = "0.1.0" +dependencies = [ + "byteorder", + "thiserror", +] + [[package]] name = "um_cli" version = "0.1.0" @@ -446,6 +454,7 @@ dependencies = [ "anyhow", "console_error_panic_hook", "getrandom", + "um_audio", "umc_kuwo", "umc_ncm", "umc_qmc", diff --git a/Cargo.toml b/Cargo.toml index 2b57826..bc15fc5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "2" -members = ["um_crypto/*", "um_wasm", "um_cli"] +members = ["um_audio", "um_crypto/*", "um_wasm", "um_cli"] [profile.release.package.um_wasm] # Tell `rustc` to optimize for small code size. diff --git a/um_audio/Cargo.toml b/um_audio/Cargo.toml new file mode 100644 index 0000000..901de54 --- /dev/null +++ b/um_audio/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "um_audio" +version = "0.1.0" +edition = "2021" + +[dependencies] +byteorder = "1.5.0" +thiserror = "1.0.63" diff --git a/um_audio/src/lib.rs b/um_audio/src/lib.rs new file mode 100644 index 0000000..883d45b --- /dev/null +++ b/um_audio/src/lib.rs @@ -0,0 +1,131 @@ +mod metadata; + +use std::fmt::Display; +use thiserror::Error; + +#[derive(Error, Debug, Clone)] +pub enum AudioError { + #[error("Require at least {0} bytes of header.")] + NeedMoreHeader(usize), +} + +pub const MASK_LOSSLESS: u32 = 0x80000000; + +#[repr(u32)] +#[derive(Debug, PartialEq, Eq)] +pub enum AudioType { + Unknown = 0, + + // Lossy + OGG = 1, + AAC = 2, + MP3 = 3, + M4A = 4, + M4B = 5, + MP4 = 6, + WMA = 7, // While possible, it is rare to find a lossless WMA file. + + // Lossless + FLAC = MASK_LOSSLESS | 1, + DFF = MASK_LOSSLESS | 2, + WAV = MASK_LOSSLESS | 3, + APE = MASK_LOSSLESS | 5, +} + +impl AudioType { + pub fn as_str(&self) -> &str { + match self { + AudioType::OGG => "ogg", + AudioType::AAC => "aac", + AudioType::MP3 => "mp3", + AudioType::M4A => "m4a", + AudioType::M4B => "m4b", + AudioType::MP4 => "mp4", + AudioType::WMA => "wma", + AudioType::FLAC => "flac", + AudioType::DFF => "dff", + AudioType::WAV => "wav", + AudioType::APE => "ape", + + _ => "bin", + } + } +} + +impl Display for AudioType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} + +fn is_mp3(magic: u32) -> bool { + // Frame sync should have the first 11 bits set to 1. + const MP3_AND_MASK: u32 = 0b1111_1111_1110_0000u32 << 16; + const MP3_EXPECTED: u32 = 0b1111_1111_1110_0000u32 << 16; + + (magic & MP3_AND_MASK) == MP3_EXPECTED +} + +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 +} + +const MAGIC_FLAC: [u8; 4] = *b"fLaC"; +const MAGIC_OGG: [u8; 4] = *b"OggS"; +const MAGIC_DFF: [u8; 4] = *b"FRM8"; +const MAGIC_WMA: [u8; 4] = *b"\x30\x26\xB2\x75"; +const MAGIC_WAV: [u8; 4] = *b"RIFF"; +const MAGIC_APE: [u8; 4] = *b"MAC "; + +pub fn detect_audio_type(buffer: &[u8]) -> Result { + let offset = metadata::get_header_metadata_size(buffer, 0)?; + if buffer.len() < offset + 0x10 { + Err(AudioError::NeedMoreHeader(offset + 0x10))?; + } + let buffer = &buffer[offset..]; + let mut magic = [0u8; 4]; + magic.copy_from_slice(&buffer[..4]); + match magic { + MAGIC_FLAC => return Ok(AudioType::FLAC), + MAGIC_OGG => return Ok(AudioType::OGG), + MAGIC_DFF => return Ok(AudioType::DFF), + MAGIC_WMA => return Ok(AudioType::WMA), + MAGIC_WAV => return Ok(AudioType::WAV), + MAGIC_APE => return Ok(AudioType::APE), + _ => {} + } + let magic = u32::from_be_bytes(magic); + if is_aac(magic) { + return Ok(AudioType::AAC); + } else if is_mp3(magic) { + return Ok(AudioType::MP3); + } + + // MP4 Containers + if &buffer[0x04..0x08] == b"ftyp" { + let mut magic = [0u8; 4]; + magic.copy_from_slice(&buffer[0x08..0x0c]); + match &magic { + // MSNV: SonyPSP + // isom / iso2: MP4 (Generic?) + b"isom" | b"iso2" | b"MSNV" => return Ok(AudioType::MP4), + b"NDAS" => return Ok(AudioType::M4A), // Nero Digital AAC Audio + _ => {} + }; + + let mut magic = [0u8; 3]; + magic.copy_from_slice(&buffer[0x08..0x0b]); + match &magic { + b"M4A" => return Ok(AudioType::M4A), // iTunes AAC-LC Audio + b"M4B" => return Ok(AudioType::M4B), // iTunes AAC-LC Audio + b"mp4" => return Ok(AudioType::MP4), // QQMusic + _ => {} + }; + } + + Ok(AudioType::Unknown) +} diff --git a/um_audio/src/metadata.rs b/um_audio/src/metadata.rs new file mode 100644 index 0000000..0a3a6a9 --- /dev/null +++ b/um_audio/src/metadata.rs @@ -0,0 +1,75 @@ +use crate::AudioError; +use byteorder::{ByteOrder, BE, LE}; + +fn parse_id3_sync_safe_int(buffer: &[u8]) -> i32 { + const UNSAFE_INT_MASK_32: u32 = 0x80808080; + const U32_BYTE_MASK_1: u32 = 0xFF000000; + const U32_BYTE_MASK_2: u32 = 0x00FF0000; + const U32_BYTE_MASK_3: u32 = 0x0000FF00; + const U32_BYTE_MASK_4: u32 = 0x000000FF; + + let value = BE::read_u32(buffer); + + // Sync safe int should use only lower 7-bits of each byte. + if (value & UNSAFE_INT_MASK_32) != 0 { + return 0; + } + + let value = (value & U32_BYTE_MASK_1) >> 3 + | (value & U32_BYTE_MASK_2) >> 2 + | (value & U32_BYTE_MASK_3) >> 1 + | (value & U32_BYTE_MASK_4); + + value as i32 +} + +const MIN_ID3_HEADER_LEN: usize = 10; + +fn get_id3_header_size(buffer: &[u8], offset: usize) -> Result { + if buffer.len() < MIN_ID3_HEADER_LEN { + Err(AudioError::NeedMoreHeader(offset + MIN_ID3_HEADER_LEN))?; + } + + // TAG: ID3v1, 128 bytes + if buffer.starts_with(b"TAG") { + return Ok(128); + } + + // ID3: ID3v2 + if buffer.starts_with(b"ID3") { + // offset value + // 0 header('ID3') + // 3 uint8_t(ver_major) uint8_t(ver_minor) + // 5 uint8_t(flags) + // 6 uint32_t(inner_tag_size) + // 10 byte[inner_tag_size] id3v2 data + // ?? byte[*] original_file_content + let inner_size = parse_id3_sync_safe_int(&buffer[6..10]) as usize; + return Ok(10 + inner_size); + } + + Ok(0) +} + +const MIN_APE_V2_HEADER_LEN: usize = 32; +fn get_ape_v2_size(buffer: &[u8], offset: usize) -> Result { + if buffer.len() < MIN_APE_V2_HEADER_LEN { + Err(AudioError::NeedMoreHeader(offset + MIN_APE_V2_HEADER_LEN))?; + } + + if buffer.starts_with(b"APETAGEX") { + let extra_size = LE::read_u32(&buffer[0x0c..0x10]) as usize; + return Ok(MIN_APE_V2_HEADER_LEN + extra_size); + } + + Ok(0) +} + +pub fn get_header_metadata_size(buffer: &[u8], offset: usize) -> Result { + let len = get_id3_header_size(buffer, offset)?; + if len == 0 { + Ok(len) + } else { + get_ape_v2_size(buffer, offset) + } +} diff --git a/um_wasm/Cargo.toml b/um_wasm/Cargo.toml index 120da76..82adb91 100644 --- a/um_wasm/Cargo.toml +++ b/um_wasm/Cargo.toml @@ -26,6 +26,7 @@ console_error_panic_hook = { version = "0.1.7", optional = true } umc_kuwo = { path = "../um_crypto/kuwo" } umc_ncm = { path = "../um_crypto/ncm" } umc_qmc = { path = "../um_crypto/qmc" } +um_audio = { path = "../um_audio" } [dev-dependencies] wasm-bindgen-test = "0.3.34" diff --git a/um_wasm/src/exports/audio.rs b/um_wasm/src/exports/audio.rs new file mode 100644 index 0000000..cee18ed --- /dev/null +++ b/um_wasm/src/exports/audio.rs @@ -0,0 +1,31 @@ +use um_audio::{AudioError, AudioType}; +use wasm_bindgen::prelude::wasm_bindgen; +use wasm_bindgen::JsError; + +/// Detected audio result +#[wasm_bindgen(js_name=AudioTypeResult)] +pub struct AudioTypeResult { + /// When this field is not zero, it means we need to feed this amount of bytes to the detector. + #[wasm_bindgen(js_name=needMore)] + pub need_more: usize, + + /// Audio extension, without "." + /// When is unknown, it will return "bin". + #[wasm_bindgen(getter_with_clone,js_name=audioType)] + pub audio_type: String, +} + +/// Detect audio type for given file header. +/// Recommended a buffer of 1024 bytes. +#[wasm_bindgen(js_name=detectAudioType)] +pub fn detect_audio_type(buffer: &[u8]) -> Result { + let (need_more, audio_type) = match um_audio::detect_audio_type(buffer) { + Ok(t) => (0, t), + Err(AudioError::NeedMoreHeader(n)) => (n, AudioType::Unknown), + // Err(err) => Err(JsError::new(err.into()))?, + }; + Ok(AudioTypeResult { + need_more, + audio_type: audio_type.to_string(), + }) +} diff --git a/um_wasm/src/exports/mod.rs b/um_wasm/src/exports/mod.rs index c9f50b9..f1e7fa6 100644 --- a/um_wasm/src/exports/mod.rs +++ b/um_wasm/src/exports/mod.rs @@ -1,3 +1,4 @@ +pub mod audio; pub mod kuwo; pub mod ncm; pub mod qmc;