elixirでMP3のID3v2タグ抽出

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

# mp3とは
言わずとしれた音楽の圧縮形式です.ぶっちゃけ細かいことはよく分かりません.
あと,それを調べてみようかなと言うのがこのプログラムを書いた動機だったりします.

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

![mp3tags.png](https://qiita-image-store.s3.amazonaws.com/0/96373/9628e1be-196a-afbd-a3be-78d9c6895f6a.png) 
[Y-Lab. Electronicsさん](http://eleken.y-lab.org/report/other/mp3tags.shtml)より拝借

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

具体的には,
- ヘッダ識別子(なんというフォーマット(ID3v2, ID3v1, MPEG Frameなど)なのか)
- ID3v2のバージョン
- 曲名
- アーティスト名
- アルバム名

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

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

ID3v2は,

![mp3tags02.png](https://qiita-image-store.s3.amazonaws.com/0/96373/62d1e649-ad21-c600-ce30-8807439d5d74.png)
[Y-Lab. Electronicsさん](http://eleken.y-lab.org/report/other/mp3tags.shtml)より拝借

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

細かいところは[Y-Lab. Electronicsさん](http://eleken.y-lab.org/report/other/mp3tags.shtml)のページを参考にしましたので,見てみてください.
あと 実装はnaoya@githubさんの[Elixir のバイナリデータに対するパターンマッチで AIFF を解析する](http://qiita.com/naoya@github/items/1fb72638989e50bbfad2)を参考にしています.

# ヘッダ部 抽出 実装
ID3v2のヘッダは

|   name  | size(byte) | detail |
|:------- |------------|--------|
| tag     |          3 | どんな情報のヘッダなのか(ID3v2は"ID3"が入力されています) |
| version |          2 | ID3v2のバージョン |
| flg     |          1 | 今回は無視しています |
| size    |          4 | ID3v2ヘッダ以下のデータサイズかと思われるのですがどうも違うようなので検証してみます |
| total   |         10 |        |

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



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

```elixir: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変数にという形で,値を束縛することができます.

最終的には ヘッダ フレーム それ以下のバイナリデータ に分けるため
それぞれをモジュール単位で抽出しようと思い分けたものが以下です.

```elixir: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](https://qiita-image-store.s3.amazonaws.com/0/96373/6d7c3dfe-857e-de15-0965-f44335f49fd4.png)

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

フレームヘッダは

|   name  | size(byte) | detail |
|:------- |------------|-------------------------------:|
| tag     |          4 | ID3v2フレームのタグはかなりの種類があるので[wikipedia](https://ja.wikipedia.org/wiki/ID3%E3%82%BF%E3%82%B0)を参照してください |
| 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 ２つに対応しています.
[mp3_analysis](https://github.com/kokoax/mp3_analysis)

# 参考文献
[MP3ファイルのタグについて (+MP4)](http://eleken.y-lab.org/report/other/mp3tags.shtml)
[Elixir のバイナリデータに対するパターンマッチで AIFF を解析する](http://qiita.com/naoya@github/items/1fb72638989e50bbfad2)