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;