ID3 タグとは ?
ID3 タグとは, オーディオデータの圧縮形式である MP3 に書き込まれている, 楽曲タイトルやアーティスト名などのメタデータのことです.
ID3 タグにはいくつかのバージョンが存在し, 大きくは バージョン 1.x とバージョン 2.x に分類されます.
ID3 タグの構造
ID3 タグの構造はバージョンによって異なるため, この記事では, hls.js の demuxer で利用されている, バージョン 2.x.x について記載します.
ヘッダー
注意すべき点はフレームのサイズで, 4 bytes のバイト列ですが, それぞれ 1 byte の先頭ビット (MSB: Most Significant Bit) は常に 0 で, 意味をもっていません (つまり, 先頭以外の 7 ビットが意味をもっています).
offset | Size | Description |
---|---|---|
0 - 2 | 3 bytes | ID3 の固定文字列 |
3 - 4 | 2 bytes | バージョン |
5 | 1 bytes | フラグ |
6 - 9 | 4 bytes | フレームのサイズ |
フラグ
フラグは, 1 byte の構造で, それぞれビットの意味は以下のようになります.
offset | Size | Description |
---|---|---|
0 - 3 | 4 bytes | 未使用 |
4 | 1 bytes | フッターが存在するか (v2.4.x 以降) |
5 | 1 bytes | タグが試験的なものか (v2.3.x 以降) |
6 | 1 bytes | タグが圧縮されているか (v2.2.x 以前) 拡張ヘッダが存在するか (v2.3.x 以降) |
7 | 1 bytes | 非同期処理がされているか |
フレームデータ
ここでは, hls.js の demuxer で利用されている, v2.3.x 以降のフレームデータ構造に関して記載します.
offset | Size | Description |
---|---|---|
0 - 3 | 4 bytes | フレーム ID |
4 - 7 | 4 bytes | フレームサイズ |
8 - 9 | 2 bytes | フラグ |
10 - | 可変 | フレームデータ |
フッター
フレーム領域のあとに追加されます. 0 - 2 ビット目の固定文字列が異なる以外, ヘッダーと同じ構造です.
offset | Size | Description |
---|---|---|
0 - 2 | 3 bytes | 3DI の固定文字列 |
3 - 4 | 2 bytes | バージョン |
5 | 1 bytes | フラグ |
6 - 9 | 4 bytes | フレームのサイズ |
コードリーディング
hls.js の /src/demux/id3.js のコードをリーディングしてみましょう.
ID3 タグのヘッダーが存在するかどうかを判定するメソッドです. 第 1 引数は, Uint8Array
, 第 2 引数はオフセットですが, オフセットはとりあえず 0
と考えるとリーディングしやすいでしょう.
offset
とヘッダーのサイズである 10 bytes を加算して, ID3 タグ全体のサイズを超えていないか判定して, ヘッダーの解析をします.
0 - 3 ビットまでをチェックして, 'ID3'
の文字列が含まれているか判定します (0x49
, 0x44
, 0x33
はそれぞれ, I
, D
, 3
の文字コードです).
次に, バージョンが, 65535
((0xFF << 8) | (0xFF << 0)
) 未満かどうかを判定します.
フラグの判定はスキップして, 最後に 6 - 9 bytes までの 4 bytes のバリデーションを実行します. 先ほども記載したように, MSB は常に 0
で意味をもっていないので, 1 byte が 127
(0x80
) 未満かどうかを判定します (また, このコードからビッグエンディアンで格納されていることがわかります)..
すべてのバリデーションが true
であれば, ヘッダーが正常であることが判定できます.
static isHeader (data, offset) {
/*
* http://id3.org/id3v2.3.0
* [0] = 'I'
* [1] = 'D'
* [2] = '3'
* [3,4] = {Version}
* [5] = {Flags}
* [6-9] = {ID3 Size}
*
* An ID3v2 tag can be detected with the following pattern:
* $49 44 33 yy yy xx zz zz zz zz
* Where yy is less than $FF, xx is the 'flags' byte and zz is less than $80
*/
if (offset + 10 <= data.length) {
// look for 'ID3' identifier
if (data[offset] === 0x49 && data[offset + 1] === 0x44 && data[offset + 2] === 0x33) {
// check version is within range
if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
// check size is within range
if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
return true;
}
}
}
}
return false;
}
フレームデータ
ここで, 汎用的に利用されている, _readSize
メソッドについて簡単に説明しておきます (offset
は 0
で考えます).
& 0x7f
のマスク処理は, 128
をオーバーしていれば 0
にしてしまう処理です. MSB は意味をもたないので, 各 byte の最大値は 127
となるからです.
サイズはビッグエンディアンで格納されているので, 1 byte 目は 21 (7 bits * 3)
bits 左算術シフト, 同様に, 2 byte 目は, 14 (7 bits * 2)
bits 左算術シフト … とシフト演算して, 最後に, ビットごとの OR 演算子を適用するとサイズが取得できます (7 bits ごとのシフトなのは, MSB が意味をもっていないからです).
static _readSize (data, offset) {
let size = 0;
size = ((data[offset] & 0x7f) << 21);
size |= ((data[offset + 1] & 0x7f) << 14);
size |= ((data[offset + 2] & 0x7f) << 7);
size |= (data[offset + 3] & 0x7f);
return size;
}
フレームデータをスライスして返すメソッドです. フレーム ID とフレームサイズの取得処理も含まれています. フレームデータが格納されているのは, 10 bytes 目からなので, Uint8Array#subarray
の第 1 引数に 10
, 第 2 引数に 10
+ フレームデータのサイズ
を指定すれば, ID3 タグのフレームデータの Uint8Array
が取得できます.
static _getFrameData (data) {
/*
Frame ID $xx xx xx xx (four characters)
Size $xx xx xx xx
Flags $xx xx
*/
const type = String.fromCharCode(data[0], data[1], data[2], data[3]);
const size = ID3._readSize(data, 4);
// skip frame id, size, and flags
let offset = 10;
return { type, size, data: data.subarray(offset, offset + size) };
}
フッター
フッターの判定も, ヘッダーの判定と本質的に同じです ('ID3'
ではなく, '3DI'
となるだけです).
static isFooter (data, offset) {
/*
* The footer is a copy of the header, but with a different identifier
*/
if (offset + 10 <= data.length) {
// look for '3DI' identifier
if (data[offset] === 0x33 && data[offset + 1] === 0x44 && data[offset + 2] === 0x49) {
// check version is within range
if (data[offset + 3] < 0xFF && data[offset + 4] < 0xFF) {
// check size is within range
if (data[offset + 6] < 0x80 && data[offset + 7] < 0x80 && data[offset + 8] < 0x80 && data[offset + 9] < 0x80) {
return true;
}
}
}
}
return false;
}