Edited at

elixirでMP3のID3v2タグ抽出

More than 1 year has passed since last update.


概要

elixir を使ってmp3の情報を格納しているid3v2というヘッダーを取得してみました.


mp3とは

言わずとしれた音楽の圧縮形式です.ぶっちゃけ細かいことはよく分かりません.

あと,それを調べてみようかなと言うのがこのプログラムを書いた動機だったりします.


id3v2とは

mp3の情報を保持しているヘッダはいくつかに別れていて,

mp3tags.png

Y-Lab. Electronicsさんより拝借

こんな感じになっています.

具体的には,

- ヘッダ識別子(なんというフォーマット(ID3v2, ID3v1, MPEG Frameなど)なのか)

- ID3v2のバージョン

- 曲名

- アーティスト名

- アルバム名

などの情報を特定のフォーマットで保持しています.

今回は,ID3v2 tag の部分のみ抽出します.

ID3v2は,

mp3tags02.png

Y-Lab. Electronicsさんより拝借

こんな感じになっていて,今回 Extended Header はあまり利用されないということで,無視しています.

細かいところはY-Lab. Electronicsさんのページを参考にしましたので,見てみてください.

あと 実装はnaoya@githubさんのElixir のバイナリデータに対するパターンマッチで AIFF を解析するを参考にしています.


ヘッダ部 抽出 実装

ID3v2のヘッダは

name
size(byte)
detail

tag
3
どんな情報のヘッダなのか(ID3v2は"ID3"が入力されています)

version
2
ID3v2のバージョン

flg
1
今回は無視しています

size
4
ID3v2ヘッダ以下のデータサイズかと思われるのですがどうも違うようなので検証してみます

total
10

の合計10byteで構成されています.

そして elixirはbitレベルで パターンマッチング 束縛 ができるので,


header.ex

<<

tag :: bitstring-size(24),
version :: bitstrint-size(16),
flg :: bitstring-size(8),
size :: unsigned-integer-size(32),
data :: binary, # 以下のバイナリデータをまとめて取得
>> = music_binary_data

こんな感じで music_bianry_dataの先頭24bit(3byte)をtag変数にその次(25bit)から16bit(2byte)をversion変数にという形で,値を束縛することができます.

最終的には ヘッダ フレーム それ以下のバイナリデータ に分けるため

それぞれをモジュール単位で抽出しようと思い分けたものが以下です.


analisis.ex

defmodule MP3Data do

defstruct [:header, :frames, :other_data]
def analisis(
<<
tag :: bitstring-size(24),
version :: bitstrint-size(16),
flg :: bitstring-size(8),
size :: unsigned-integer-size(32),
data :: binary, # 以下のバイナリデータをまとめて取得
>>) do
%MP3Data {
:header MP3Header(tag, version, flg, size)
}
end
end

defmodule MP3Header do
defstruct [:tag, :version, :flg, :size]
def getHeader(tag, version, flg, size) do
%MP3Header{
tag: tag,
version: version,
flg: flg,
size: size
}
end
end

# music_binary_data はファイル読み込みなどで用意します.
MP3Data.analisis( music_binary_data )



フレーム部 抽出 実装

今回以下に紹介するのはID3v2.3のものです.

フレーム部実装は結構めんどうでフレームの数が決まっていません.

frames.png

なので こんな感じで最低一つのフレームが並んでいます.

フレームヘッダは

name
size(byte)
detail

tag
4
ID3v2フレームのタグはかなりの種類があるのでwikipediaを参照してください

size
4
フレームヘッダのサイズを除いたフレーム 全体のサイズ

flg
2
今回は無視しています

total
10

の合計10byteで構成されています.

name
size(bit)
detail

0
8(1byte)
何故かフレームヘッダ直下の1byteが0になっていましたので捨ててます.

data
フレームヘッダの(size-1)
フレームヘッダのsize-1(bit)のデータです タイトル名や制作年などが入ります

フレームのデータのサイズは決まっておらず32bitの非負整数で表現されています.

実装は 多重再帰を使いました.

流れとしては

getFrames でフレームヘッダを抽出



getFrameData でフレームのデータを抽出



getFrames で次のフレームヘッダを抽出



getFrameData でフレームのデータを抽出



....

そしてフレームヘッダのタグは 0-9かA-Z で構成されているので

それ以外の文字が入っていた場合はフレームではないので終了する仕組みにしています.


analisis.ex

defmodule MP3Data do

defstruct [:header, :frames, :sound_data]
def analisis(
<<
tag :: bitstring-size(24),
version :: bitstrint-size(16),
flg :: bitstring-size(8),
size :: unsigned-integer-size(32),
data :: binary, # 以下のバイナリデータをまとめて取得
>>) do
%MP3Data {
header: MP3Header.getHeader(),
frames: MP3Frames.getFrame(data),
}
end
end

defmodule MP3Frames do
# フレームのデータ部を抽出
defp getFrameData(size, next) do
<<
_ :: bitstring-size(8), # すいませんここなぜか0でしたので捨ててます.
data :: bitstring-size(size), # フレームのデータ
other :: binary,
>> = next
[data, getFrame(other)]
end

# ここではフレームヘッダの情報を抽出
def getFrame(
<<
tag :: bitstring-size(32), # このフレームに記述されているデータの種類
size :: unsigned-integer-size(32), # フレームヘッダ以下のフレームのbitサイズ
_ :: bitstring-size(16), # フレームのフラグ 今回は使わないので捨ててます
next :: binary,
>>) do

# フレームのタグがありえないものだったら
# それ以降フレームが存在しないということで終了
if Regex.match?(~r/^[0-9A-Z]+$/, tag) do

[ret, map] = getFrameData((size-1)*8, next)
if map == nil do
%{tag => ret}
else
Map.merge(%{tag => ret}, map)
end
end
end
end


こんな感じで実装しました.


まとめ

MP3の情報を取り出せました.

しかし ID3v2 以下にはMPEG Frameがあるがまだ抽出できていないのでMP3のタグ抽出が終わったとはいえないので続けてMPEG Frameの抽出をしようと思います.


データ

githubにソースを上げています.

こちらは ID3v2.2 と ID3v3 2つに対応しています.

mp3_analysis


参考文献

MP3ファイルのタグについて (+MP4)

Elixir のバイナリデータに対するパターンマッチで AIFF を解析する