つらつら Excel VBA

私の備忘録です。

mp3バイナリ解析準備

必要なものを作っていく。


16進数と2進数の10進数変換

マイナスや小数点以下の数値、エラーを考慮せずに作ったので取扱注意。

'16進数を2進数に変換
Function HexToBin(sHex As String) As String
    Dim rtnStr As String: rtnStr = ""
    Dim i As Long, tmp As String
    
    For i = Len(sHex) To 1 Step -1
        tmp = WorksheetFunction.Hex2Bin(Mid(sHex, i, 1))
        tmp = Right("000" & tmp, 4) '桁埋め
        rtnStr = tmp & rtnStr
    Next
    
    HexToBin = rtnStr
End Function
'2進数を10進数に変換
Function BinToDec(sBin As String) As Long
    Dim tmp As String
    tmp = StrReverse(sBin) '文字を逆にする
    Dim iSum As Long: iSum = 0
    Dim i As Long
    
    For i = 1 To Len(tmp)
        If Mid(tmp, i, 1) = "1" Then
            iSum = iSum + 2 ^ (i - 1)
        End If
    Next
    
    BinToDec = iSum
End Function
'16進数を10進数に変換
Function HexToDec(sHex As String) As Long
    HexToDec = BinToDec(HexToBin(sHex))
End Function


Syncsafe Integer計算用。
ID3v2系のヘッダのサイズを調べるために使う。v2.3ではヘッダのみ。他にも使われるかもしれない。

'Syncsafe Integerを計算。16進数を10進数で返却。
'最上位8ビット目をスキップして連結
Function HexToSyncsafeInteger(sHex As String) As Long
    Dim rtnStr As String: rtnStr = ""
    Dim tmp As String
    tmp = HexToBin(sHex) '16進数を2進数に変換
    tmp = StrReverse(tmp) '文字を逆にする
    
    '最上位8ビット目をスキップして連結
    Dim i As Long
    For i = 1 To Len(tmp)
        If i Mod 8 <> 0 Then rtnStr = Mid(tmp, i, 1) & rtnStr
    Next
    
    HexToSyncsafeInteger = BinToDec(rtnStr)
End Function


上記で作ったもののテスト

'引数は16進数そのまま

'アスキーコードの変換テスト
Debug.Print Chr(HexToDec("49")) '49 44 33→ID3

'ヘッダサイズ取得テスト(Syncsafe Integer)
Debug.Print HexToSyncsafeInteger("00001000") '2048バイト

'フレームサイズ取得テスト
Debug.Print HexToDec("00000019") '25バイト

'UTF-16の変換テスト
Debug.Print ChrW(HexToDec("9F8D")) '龍

'Shift-JISの変換テスト
Debug.Print Chr(HexToDec("97B4")) '龍

あとはmp3のバイナリ構造に合わせて読み出すように組めばOK。


全体の構造ざっくり

ID3v1系はファイルの末尾にタグ情報がある。当記事では取り扱わない。
ID3v2系はファイルの先頭にタグ情報がある。「49 44 33」と書かれている。これをアスキーコードで表すと「ID3」である。
ID3v2のヘッダ10バイト+ヘッダサイズ(Syncsafe Integer)の次にmp3の曲本体がある。
mp3の曲本体はFFFBから始まる。
使われていない部分は0で埋められている。


id3v2.3、id3v2.4のヘッダー
先頭10バイト
場所長さ内容備考
1~33識別子「49 44 33」が入っている。アスキーコードで「ID3」。
4~52バージョン「02 00」「03 00」「04 00」のいずれかが入っている。
それぞれv2.2、v2.3、v2.4を表す。
61フラグ非同期化、拡張ヘッダ、実験中などを表す。基本は「00」。
本記事では「00」以外のパターンは謎とする。
7~104サイズフレーム全体のサイズ。Syncsafe Integer。


id3v2.3、id3v2.4のフレーム
ヘッダーの直後、11バイト目以降
場所長さ内容備考
1~44フレームID「TIT2」「TPE1」「TALB」などが入る。
アスキーコードで書かれる。
5~84フレームサイズデータ本体のサイズが入っている。
フレームサイズ10バイトは含まれない。
9~102フラグ謎。
11~1~可変データ本体文字コード、BOM、文字の順で書かれる。


主なフレームID(v2.3)

フレームID 内容
TIT2タイトル
TPE1アーティスト
TALBアルバム
TCONジャンル
TRCKトラックNo
TYERリリース年
COMMコメント
APICアートワーク


文字コード

フレームデータの1バイト目に文字コードが書かれる。
文字コード備考
00ISO-8859-1
01UTF-16BOM有
02UTF-16BOM無(v2.4)
03UTF-8(v2.4)

00が指定されていてもShift-JISが入っていることがあるので注意。

※1バイト目からテキストデータが始まる例を確認したが、フレームIDが「T〇〇〇」とは違った。特殊な例なのだろうか。


BOM(Byte Order Mark)

文字の読み取り順を指定するもので文字データの頭に、UTF-16は「FFFE」「FEFF」のどちらか、UTF-8は「EF BB BF」が書かれる。

FFFEはリトルエンディアン、FEFFはビッグエンディアン
v2.3はUTF-16で必ずBOMが付くので、それで判断する。多分リトルエンディアン。

'UTF-16の変換テスト
Debug.Print ChrW(HexToDec("9F8D")) '龍

この例ではv2.3に「8D9F」と書かれており、前後を入れ替えて「9F8D」にした。リトルエンディアンは交互に書かれている。
ビッグエンディアンはそのままの順番で書かれる。

UTF-8のBOMは「EF BB BF」だが、そもそもUTF-8はBEかLEかの区別が無い。BOMの有無に関係無く書かれている順に処理する。


文字列の終了フラグ

UTF-16の文字の終わりは「00 00」。
Shift-JISの文字の終わりは「00」。
UTF-8の文字の終わりは「00」。

最終バイト以外で終了フラグが出る場合は以降を無視するらしい。フレームサイズに注意して読む。


UTF-8

16進数を2進数にして、最初の1バイト目を見る。

最初のビットが0なら1バイト文字。以下7ビットが有効。
最初のビットが110なら2バイト文字。以下5ビット+次の1バイトの下6ビットが有効。
最初のビットが1110なら3バイト文字。以下4ビット+次と次の1バイトの下6(略)。
最初のビットが11110なら4バイト文字。以下3ビット+次と次と次の(略)
最初のビットが10なら、最初のビットではない。
上記以外なら、UTF-8ではない。


※表のx部分が有効ビット
nバイト文字1バイト2バイト3バイト4バイト
10xxxxxxx
2110xxxxx10xxxxxx
31110xxxx10xxxxxx10xxxxxx
411110xxx10xxxxxx10xxxxxx10xxxxxx

有効ビットを全て繋げて16進数に変換すればユニコード番号になる。
10進数に変換してChrW変換をかければ文字が出る。


例)E9BE8D
11101001 10111110 10001101
1001 111110 001101→1001 1111 1000 1101
→9F8D(10進数で40845)
→龍


以下、UTF-8変換テストコード

'テスト。「龍」が出る予定。
Private Sub test_getStringUTF8()
    
    Dim testString(1 To 3) As String
    testString(1) = "E9"
    testString(2) = "BE"
    testString(3) = "8D"
    
    Dim iStart As Long: iStart = 1
    Dim iLen As Long: iLen = 3
    
    Dim sFrameVal As String: sFrameVal = ""
    Dim tmp As String, tmpBin As String
    Dim i As Long, k As Long, iByte As Long
    Dim iEnd As Long: iEnd = (iStart + iLen - 1)
    
    For i = iStart To iEnd
        
        If testString(i) = "00" Then Exit For '終了フラグ
        
        iByte = NumByteUTF8(testString(i))
        If iByte = 0 Then Exit For
        
        Select Case iByte
        Case 1
            tmpBin = Right(HexToBin(testString(i)), 7)
            
        Case 2 To 6
            tmpBin = Right(HexToBin(testString(i)), 7 - iByte)
            
            For k = 1 To iByte - 1
                tmp = Right(HexToBin(testString(i + k)), 6)
                tmpBin = tmpBin + tmp
            Next
            
        Case Else
            '到達不可
            
        End Select
        
        tmp = ChrW(BinToDec(tmpBin))
        sFrameVal = sFrameVal & tmp
        i = i + iByte - 1
        
    Next
    
    Debug.Print sFrameVal '龍
    
End Sub


'UTF-8のバイト数を判定。
Private Function NumByteUTF8(sHex1 As String) As Integer
    
    Dim rtnInt As Integer
    Dim sBin As String: sBin = HexToBin(sHex1)
    
    Select Case True
    Case Left(sBin, 2) = "10"
        rtnInt = 0
        Debug.Print "先頭バイトではありません(" & sHex1 & ")"
    Case Left(sBin, 1) = "0"
        rtnInt = 1
    Case Left(sBin, 3) = "110"
        rtnInt = 2
    Case Left(sBin, 4) = "1110"
        rtnInt = 3
    Case Left(sBin, 5) = "11110"
        rtnInt = 4
    Case Left(sBin, 6) = "111110"
        rtnInt = 5
    Case Left(sBin, 7) = "1111110"
        rtnInt = 6
    Case Else
        rtnInt = 0
        Debug.Print "判別不能(" & sHex1 & ")"
    End Select
    
    NumByteUTF8 = rtnInt
    
End Function


Shift-JIS

1~2バイト文字。
バイナリから復元するには1~2バイト文字の判別が必要。

'Shift-JISのバイト数を判定。1or2バイト。超無理矢理。
'2バイト文字の1バイト目は0x81~9F、0xE0~EF。
'2バイト文字の2バイト目は0x40~7F、0x80~FC。
'これ以外を1バイト文字と判定。
Function Is2byteShiftJIS(sHex1 As String, sHex2 As String) As Boolean
    
    Is2byteShiftJIS = False
    If (HexToDec("81") <= HexToDec(sHex1) And HexToDec(sHex1) <= HexToDec("9F")) Or _
       (HexToDec("E0") <= HexToDec(sHex1) And HexToDec(sHex1) <= HexToDec("EF")) Then
       If (HexToDec("40") <= HexToDec(sHex2) And HexToDec(sHex2) <= HexToDec("7F")) Or _
          (HexToDec("80") <= HexToDec(sHex2) And HexToDec(sHex2) <= HexToDec("FC")) Then
            Is2byteShiftJIS = True
        End If
    End If
    
End Function




かじった程度の知識しかないのでフラグや拡張ヘッダーについては触れません。
サロゲートペア文字は知らなかったことにして回避しました。

次の記事でmp3タグ解析用(v2)のプログラムを書きます。

以上。