Parse ID3v2 tag với Rust
Rust
27
mp3
1
White

Giang Nguyen viết ngày 24/02/2017

Đôi dòng về ID3

alt text

Nếu bạn nào chưa biết thì có thể đọc phần này, hoặc biết rồi thì có thể next tới phần kế tiếp nhé.

1. Giới thiệu

Như hình trên thì mỗi bài nhạc thì đều có các thông tin cơ bản như Title, Album, Composers, Cover ... và rất nhiều các thông tin khác. Để cho việc quản lý đơn giản thì tất cả các thông tin này đều được lưu ở ID3.

Những thông tin này có cần thiết hay quan trọng không ?

Điều đó tuỳ thuộc vào mục đích sử dụng, đơn giản như, nếu một track của bạn đầy đủ các thông tin thì sẽ dể dàng giúp cho player tự sắp xếp, filter các kiểu.

Vậy ID3 được lưu như thế nào ?

2. Cách lưu của ID3

Có 2 version khác nhau của ID3: ID3v1 vs ID3v2, mỗi version có một kiểu store khác.

Tất cả các version của ID3 dùng với những định dạng được encode theo các chuẩn: MPEG-1/2 layer I, MPEG-1/2 layer II, MPEG-1/2 layer III and MPEG-2.5.

  • ID3v1 sẽ dùng 128 bytes ở cuối mỗi track để store các thông tin và con số này sẽ fixed.
  • ID3v2 sẽ có cách store flexible hơn thay vì ở cuối thì ID3v2 sẽ store ở đầu và size của tag có thể lên tới 256MB.

Với ID3v2 thì sẽ có 3 version khác nhau: ID3v2.2, ID3v2.3, ID3v2.4 mỗi version lại có cách tổ chức khác nhau, ở đây mình sẽ đi vào phiên bản ID3v2.3.

2.1 ID3v2.3

Bitorder đối với ID3v2 là most significant bit first (MSB) và byteorder là most significant byte first. 2 thằng này là gì bạn có thể tìm hiểu ở đây

Ghi chú: $00 là cách biểu diễn của dạng hexa, %00 là cách biểu diễn của binary.

2.1.1 ID3v2 Header

Phần này sẽ cho biết những thông tin cơ bản của tag như version, size của tag, ...

Header của ID3v2 dùng 10 bytes để store.

ID3v2/file identifier "ID3" 
ID3v2 version $03 00 
ID3v2 flags %abc00000 
ID3v2 size 4 * %0xxxxxxx 

Nếu bạn parse 1 track mp3 thì 10 bytes đầu sẽ là ID3 header, 3 bytes đầu tiên để xác định identifier đối với ID3 thì 3 bytes này luôn là ID3, nếu ra cái khác thì nhớ tìm hiễu coi lại có đúng không nhé. 2 bytes kế tiếp để quy định version, byte đầu quy định major version như trên thì là 3, byte kế tiếp là revision, đối với trên thì là 0, tổng hợp 5 bytes đầu ta có đầy đủ version của ID3: ID3v2.3.0

Byte kế tiếp sau version sẽ quy định các flags, hiện tại mình sẽ không đi sâu vào những flags này. Với flags này thì mỗi version của ID3v2 sẽ có số lượng bit khác nhau như ID3v2.2 sẽ có 2 bit thôi %ab000000, ID3v2.3 thì 3 bit, với ID3v2.4 thì sẽ tới 4 bit %abcd0000. Bài viết kế tiếp mình sẽ đi sâu những flags này.

Kế tiếp là field size, filed này sẽ quy định size của tag, như đã nói ở trên thì ID3v1 chỉ có 128 bytes, với ID3v2 size có thể lên tới 256MB. Field size dùng 4 bytes để store. Tuy nhiên chỉ có 28 bits được dùng để store size, tại sao lại 28 bits. Bạn có thể đọc thêm về Synchsafe hoặc Wiki Synchsafe hoặc Why are Synch safe integer.

4 * %0xxxxxxx ở đây là 4 bytes mỗi byte theo kiểu MSB nhé.

Ok! fine. Bắt đầu code parse phần Header nào ;)

Khởi tạo project

Để khởi tạo một project ở rust cách đơn giản nhất là dùng cargo.

cargo new --bin parse-id3

Lưu ý: chúng ta cần một chương trình runable hơn lib nên dùng --bin, vì bài viết này chỉ phục vụ cho PoC.

Bạn có thể dùng bất cứ editor nào để code Rust, mình thì dùng vscode.

Việc đầu tiên cần làm open một track dưới định dạng .mp3, hiện tại mình chỉ test trên định dạng .mp3, mình chưa test trên những định dạng khác như wav, flac nhưng nếu những định dạng khác được encode bằng mpeg thì sẽ parse được ID3. Bạn có thể dùng bất cứ bản nhạc nào bạn thích, resource ở đây mình dùng là track: Enchantress thuộc album Vanquish của Two Step From Hell.

Để open một file với Rust thì hết sức đơn giản bằng std File::open<P: AsRef<Path>>(file: P>) -> Result<File>, hàm open sẽ nhận vào một file có type là P, P này phải implement trait AsRef, nếu open thành công sẽ trả về file: File, nếu fail sẽ panic ra lỗi nếu chúng ta dùng unwrap, lưu ý type Result ở đây là alias của std::result::Result, full Path

// inside std::io module

type Result<T> = std::result::Result<T, std::io::Error>;

// or

type Result<T> = std::result::Result<T, Error>; 

để thuận tiện cho việc sử dụng track lại sau này, chúng ta có thể khai báo 1 biến để lưu path của track:

const VANQUISH_PATH: &'static str = "/Users/giangnguyen/Downloads/Two Steps from Hell - Vanquish \
                                  (2016) [320]/06 - Enchantress.mp3";

bạn thay cái path đó với đường dẫn bạn lưu track.

Vậy đoạn code để mở file trong Rust sẽ là:

use std::fs::File;

const VANQUISH_PATH: &'static str = "/Users/giangnguyen/Downloads/Two Steps from Hell - Vanquish \
                                  (2016) [320]/06 - Enchantress.mp3";

fn main() {
    let f = File::open(VANQUISH_PATH);

    match f {
        Ok(f) => println!("opened"),
        Err(err) => println!("{}", err.to_string())
    }
}

Mở terminal lên và chạy lệnh cargo run, nếu file của bạn tồn tại và đúng đường dẫn thì bạn sẽ thấy ở màn hình là dòng text : "opened". Còn nếu những điều trên sai thì bạn sẽ xuất hiện một dòng lỗi như: "No such file or directory (os error 2)".

Nếu bạn dùng unwrap function ngay sau open thì bạn sẽ nhận được một dòng panic!

use std::fs::File;

const VANQUISH_PATH: &'static str = "/Users/giangnguyen/Downloads/Two Steps from Hell - Vanquish \
                                  (2016) [320]/06 - Enchantress.mp3";

fn main() {
    let f = File::open(VANQUISH_PATH).unwrap();
}
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Error { repr: Os { code: 2, message: "No such file or directory" } }', src/libcore/result.rs:860 note: Run with `RUST_BACKTRACE=1` for a backtrace. 

Mình sẽ không đi sâu vào vấn đề handle error ở Rust vì nó rât nhiều thứ để nói. Nhưng với một chương trình cần prototype nhanh thì unwrap là lựa chọn tốt nhất, để panic lỗi.

Fine! sau bước open file sẽ bước đọc file, vậy đọc thế nào và cần nên đọc bao nhiêu buffer ?

Như đã nói ở trên thì tag size của ID3 có thể lên tới 256MB, nhiều có ai đời nào một file .mp3 lại mấy trăm MB như thế không ?, nếu có chắc đầu thằng edit cái tag đó có vấn đề, mình chí thấy một file .flac được rip từ đĩa than với bitrate trên 1400Kpb/s thì có thể lên tới dùng lượng 150MB.

Vậy chúng ta cần load vào nhiêu buffer để parse cái tag ?. Với bài viết này thì mình chỉ load tầm 100 bytes để parser 2 phần: Header + Title. Chúng ta không thể biết sẽ có bao nhiêu fields trong tag sẽ được thêm vào, hay có bao nhiêu field không có thông tin. Với những file nhỏ như .mp3 thì chúng ta có thể đọc 1 lần để load hết vào buffer.

Đề phục vụ cho việc read vào buffer thì Rust có một trait để làm việc đó là std::io::Read với function read.

pub trait Read {
    pub fn read(&mut self, buf: &mut [u8]) -> Result<usize>;
    ...
}

Đây là trait generic nên để dùng nó thì chúng ta phải xem type T có implement trait này hay không, may mắn là với type File ở trên thì có implement.

Với method read thì sẽ đọc từ source rồi đưa vào buf, trả về sẽ là số bytes đã đọc được => Ok(n), 0 =< n <= buf.len(). Bạn có thể đọc thêm về method read.

Nói sơ qua thôi giờ code tiếp.

// khai báo buffer let mut buffer = [0; 100]; 
// đọc file f.read(&mut buffer);

// in debug
println!("{:?}", buffer); 

hoặc final code

use std::fs::File;

const VANQUISH_PATH: &'static str = "/Users/giangnguyen/Downloads/Two Steps from Hell - Vanquish \
                                  (2016) [320]/06 - Enchantress.mp3";

fn main() {
    let f = File::open(VANQUISH_PATH).unwrap();
    // khai báo buffer
    let mut buffer = [0; 100];
    // đọc file
    f.read(&mut buffer);

    // in debug

    println!("{:?}", buffer);
}

Nếu bạn chạy lệnh trên thì bạn sẽ nhận được 2 lỗi to đùng

Lỗi 1

error: no method named `read` found for type `std::fs::File` in the current scope --> src/main.rs:11:7 | 11 | f.read(&mut buffer); | ^^^^ | = help: items from traits can only be used if the trait is in scope; the following trait is implemented but not in scope, perhaps add a `use` for it: = help: candidate #1: `use std::io::Read;` 

Với lỗi này thì do chúng ta quên import trait Read, import vào thì hết sức đơn giản như làm với File

use std::io::Read; ... 

Lỗi 2

error[E0277]: the trait bound `[{integer}; 100]: std::fmt::Debug` is not satisfied --> src/main.rs:15:22 | 15 | println!("{:?}", buffer); | ^^^^^^ the trait `std::fmt::Debug` is not implemented for `[{integer}; 100]` | = note: `[{integer}; 100]` cannot be formatted using `:?`; if it is defined in your crate, add `#[derive(Debug)]` or manually implement it = note: required by `std::fmt::Debug::fmt`

lỗi này là gì ạ ?. Lỗi này cho biết trait Debug hay shortcut :? dùng trong macro println! không dùng được. Why?. Quay trở về nói về type của var buffer xíu.

// implicit let mut buffer = [0; 100];

// explicit let mut buffer: [u8; 100] = [0; 100]; 

2 cách khai báo trên khác nhau nhưng đều cùng một ý nghĩa, với dạng khai báo

let mut buffer: [T; N] = [0; 100]; 

đây là kiểu array, fixed size nhé. Mỗi một phần từ trong array có chung 1 type , N chính là sẽ lượng phần tử trong array.

Như document đã chỉ rõ chỉ có N nằm trong khoản [0, 32], mặc định sẽ được implement trait Debug, với N lớn hơn sẽ không có. Detail.

Ok! vậy để thấy được rõ ràng từng bytes chúng ta đã đọc thì chỉ việc thay dạng array thì Vec<T>.

... let mut buffer: Vec<u8> = vec![0; 100]; ... 

Nếu chạy cargo run lại chúng ta vẫn còn thấy lỗi, lỗi gì đây?

error: cannot borrow immutable local variable `f` as mutable --> src/main.rs:12:5 | 8 | let f = File::open(VANQUISH_PATH).unwrap(); | - use `mut f` here to make mutable ... 12 | f.read(&mut buffer); | ^ cannot borrow mutably 

Hmm! theo trait Read thì chúng ta phải truyền vào dưới dạng &mut self, đằng này chúng ta chỉ truyền vào dưới dạng &self. Fix bằng cách sửa lại f dưới dạng mutable

... let mut f = File::open(VANQUISH_PATH).unwrap(); ... 

Chạy lại chương trình, bump, chúng ta sẽ thấy được một array chứa các bytes đã đọc được.

[73, 68, 51, 3, 0, 0, 0, 0, 17, 112, 84, 73, 84, 50, 0, 0, 0, 27, 0, 0, 1, 255, 254, 69, 0, 110, 0, 99, 0, 104, 0, 97, 0, 110, 0, 116, 0, 114, 0, 101, 0, 115, 0, 115, 0, 0, 0, 84, 65, 76, 66, 0, 0, 0, 21, 0, 0, 1, 255, 254, 86, 0, 97, 0, 110, 0, 113, 0, 117, 0, 105, 0, 115, 0, 104, 0, 0, 0, 84, 67, 79, 78, 0, 0, 0, 15, 0, 0, 1, 255, 254, 83, 0, 99, 0, 111, 0, 114, 0, 101] 

Đây là những con số chứa đầy ý nghĩa. Người thường sẽ nhìn những con số này sẽ vô nghĩa, nhưng với programmer nó sẽ nói lên rất nhiều ý nghĩa. Ý nghĩa gì thì parse ra mới biết :)). Chứ mình nhìn cũng chả biết nó là khỉ gì =)).

Nhớ lại ID3 tag sẽ bắt đầu ở mỗi track mp3, nên chúng ta sẽ đi từ đầu buffer để parse.

Parse identifier:

3 bytes đầu sẽ là identifier

...
let identifier = &buff[.. 3];
let identifier_string = String::from_utf8_lossy(&identifier);
println!("Identifier: {}", identifier_string); // Identifier: ID3
...

hoặc

...
let identifier = &buff[.. 3];
let identifier_string = String::from_utf8_lossy(&identifier);
println!("Identifier: {}", identifier_string); // Identifier: ID3
...

hoặc

...
let identifier = &buff[.. 3];
let identifier_string = String::from_utf8(identifier.to_owned()).unwrap();
println!("Identifier: {}", identifier_string); // Identifier: ID3
...

2 cách trên thì mục đích cuối cùng vẫn như nhau, nhưng cách nào an toàn hơn? Dùng from_utf8 sẽ an toàn hơn vì nó bảo đảm mỗi ký tự phải valid đối với UTF-8. Nếu không bảo đảm sẽ có lỗi. Còn với from_utf8_lossy sẽ không có gì để đảm bảo nếu ký tự của ta không valid UTF-8, nếu không valid ký tự đó sẽ được in ra ký hiệu: �

Parse version:

2 bytes kế tiếp sẽ là major và revision, nên code cũng đơn giản.

... 
let major = &buffer[3]; 
let revision = &buffer[4]; println!("Version: ID3v2.{}.{}", major, revision); ... 

Kế tiếp parse tag size:

4 bytes kế tiếp là thông tin của tag size, mình sẽ nói sơ qua về cách tính: Ví dụ cụ thế với 4 bytes:

// decimal 0 0 17 112

// binary 0000 0000 0000 0000 0001 0001 0111 0000 

Theo document ID3 thì bit 7 sẽ được set về 0 và sẽ được bỏ ra kết quả:

// decimal 0 0 17 112

// binary _000 0000 _000 0000 _001 0001 _111 0000 

Khi gôm lại tính thì ta được:

// decimal 0 0 17 112

// binary 0000000000000000100011110000

Tage size: Math.pow(2, 11) + Math.pow(2, 7) + Math.pow(2,6) + Math.pow(2, 5) + Math.pow(2,4) - 10 = 2278 bytes;

extern crate byteorder;

...
fn unsynchsafe(i: u32) -> u32 {
    let mut output = 0x0;
    let a = i & 0xff;
    let b = (i >> 8) & 0xff;
    let c = (i >> 16) & 0xff;
    let d = (i >> 24) & 0xff;

    output = output | a;
    output = output | (b << 7);
    output = output | (c << 14);
    output = output | (d << 21);
    return output;
}

fn main() {
    ...
    let bytes_size = &buffer[6..10];
    let read_u32_bytes = BigEndian::read_u32(bytes_size);
    let tag_size = unsynchsafe(read_u32_bytes) - 10;
    ...
    println!("Tag size: {} bytes", tag_size);
}
... 

Khi chạy chương trình chúng ta sẽ thấy ở màn hình:

Identifier: ID3 
Version: ID3v2.3.0 
Tag size: 2278 bytes 

Next, sẽ parse Frame, 1 tag sẽ có rất nhiều frame, nhưng lúc nào cũng phải có ít nhất một frame. Mỗi frame sẽ có:

Frame ID $xx xx xx xx (four characters) 
Size $xx xx xx xx 
Flags $xx xx 

Parse frame ID:

Lúc nãy chúng ta đã đi hết 10 bytes, giờ từ byte thứ 11 sẽ là frame.

 ... 
let bytes_frame_id = &buffer[10..14];
let frame_id = String::from_utf8(bytes_frame_id.to_owned()).unwrap();
println!("Frame ID: {}", frame_id);
...

ID3v2 quy định một số frame ID bắt đầu bằng TXXX là dạng text, như ví dụ trên, chúng ta parse ra: TTT2 là Title/songname/content description

Parse frame size:

4 bytes kế tiếp sau frame id sẽ là frame size.

... 
let bytes_frame_size = &buffer[14..18];
let read_u32_frame_size = BigEndian::read_u32(bytes_frame_size);
let frame_size = unsynchsafe(read_u32_frame_size);
println!("Frame size: {:?} bytes", frame_size);
... 

Như trên thì frame size sau khi parse là 27 bytes sẽ bao gồm text information và 1 bytes để quy định kiểu encoding. Như với track này thì kiểu encoding là Unicode 16 bit.

Byte quy định kiểu encoding nằm ngay sau 2 byte frame header flags. Đối với Unicode 16 bit bắt đầu sẽ là Unicode BOM($FF FE hoặc $FE FF), nên cần loại 2 bytes này ra.

...
// start_index = 10 bytes (header) + 10 bytes (frame header) + 1 bytes (set encoding)
+ 2 bytes Unicode BOM
// end_index = start_index + (frame_size - 1 bytes (set encoding) - 2 bytes (Unicode BOM))
let start_index = 10 + 10 + 1 + 2;
let end_index = start_index + frame_size - 1 - 2;
let bytes_text_title = &buffer[(start_index as usize)..(end_index as usize)];
println!("Title: {}",
             String::from_utf8(bytes_text_title.to_owned()).unwrap());
...

Chúng ta đã kết thúc việc parse một số thông tin cơ bản. Còn rất nhiều frame chúng ta chưa parse, đang chờ bạn parse đó, tuy nhiên trước khi chúng ta parse frame kế tiếp thì nên refactor lại code, để cho làm việc tiện hơn và khoa học.

Nếu trong quá trình refactor gặp một số trường hợp field đó có thể có giá trị NULL, thì bạn nên đọc lại bài viết Rust không có NULL thì code kiểu gì?. Một vài trường hợp có nhiều variant khác nhau thì nên dùng Enum như trường hợp: Header Flags

enum ID3v2HeaderFlags {
    Unsynchronisation,
    ExtendedHeader,
    ExperimentalIndicator
}

Phân kế tiếp mình sẽ dùng Rust để tiếp tục demux file .mkv tách audio và subtitle.

Cuối cùng đừng quên kipalog nhé.

Nguồn: Giang Nguyen

Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Giang Nguyen

20 bài viết.
34 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
30 5
(Ảnh) Hai ngày nay mình đã tìm hiểu về Amazon S3, Cloudfront và Letsencrypt để xây dựng 2 trang web static, thứ nhất là trang chủ của (Link) và t...
Giang Nguyen viết gần 2 năm trước
30 5
White
8 0
Type Result Type trên dùng để làm gì? Result được dùng cho những trường hợp chúng ta muốn return lại một giá trị nào đó (Ok) hoặc propagating erro...
Giang Nguyen viết hơn 1 năm trước
8 0
White
7 0
Mặc định Phoenix dùng (Link) để thực hiện việc như transpile từ es6 javascription, hoặc compile từ scss/sass sang css. Tuy nhiên mình có đọc qua d...
Giang Nguyen viết hơn 1 năm trước
7 0
Bài viết liên quan
White
3 3
Hôm nay tự viết lại cái note này về quá trình học Rust, xem như là tự giúp mình nhớ sâu và rõ hơn về Rust. Note này sẽ ngắn gọn hơn bản (Link). Sơ...
Giang Nguyen viết gần 2 năm trước
3 3
White
0 0
fn plus_5(x: Option) i32 { x.unwrap_or(0) + 5 } fn main() { assert_eq(15, plus_5(Some(10))); } Compile không lỗi, thay đổi chút xíu nhỏ, ...
Giang Nguyen viết 2 năm trước
0 0
White
2 4
Cài đặt Rust trên Arch Linux Việc cài đặt Rust trên môi trường Arch Linux khá là đơn giản. pacman có sẵn gói (Link) và (Link), bạn có thể chọn các...
Huy Trần viết hơn 1 năm trước
2 4
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
20 bài viết.
34 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!