動機
前回のエントリでは,音楽に合わせて何か絵を動かそうとして beat detection を行いましたが,事前に音に関する情報を分析するアプローチにも挑戦して見たいと思っていたところ,MIDI ファイルなら Elixir の勉強に丁度良いかもと思いついてこの記事を書きました.
MIDI
MIDI は音の情報の形式の一つです.MIDI の最も一般的なファイルが SMF(Standard MIDI Format)です.以降,SMF のファイルを単に「MIDI ファイル」と表記します.
Elixir
Elixir のパターンマッチは素晴らしいです.バイナリもパターンマッチ出来ます.
iex(1)> <<header :: 8, data :: binary>> = <<1, 2, 3, 4, 5>>
<<1, 2, 3, 4, 5>>
iex(2)> header
1
iex(3)> data
<<2, 3, 4, 5>>
やってみる
こちらを参考に MIDI ファイルの中身を覗いてみる.
まず,MIDI ファイルを読み込みます.
iex(1) {:ok, data} = File.read 'sample.midi'
{:ok,
<<77, 84, 104, 100, 0, 0, 0, 6, 0, 1, 0, 20, 1, 224, 77, 84, 114, 107, 0, 0, 0,
36, 0, 255, 3, 3, 82, 101, 105, 0, 255, 81, 3, 7, 161, 32, 0, 255, 88, 4, 4,
2, 24, 8, 143, 143, 0, 255, 88, ...>>}
バイナリさんこんにちわ.MIDI ファイルは必ずヘッダチャンクとトラックチャンクで構成されています.
ヘッダ
ヘッダを覗いてみます.ヘッダのチャンク構成は以下のようになっています.括弧の数字はバイト数です.
<“MThd”> <データ長 (4)> <フォーマット (2)> <トラック数 (2)> <時間単位 (2)>
パターンマッチを使うと一発でヘッダをパース出来ます.
iex(2)> <<"MThd", 6 :: size(32), format :: size(16), num_of_tracks :: size(16), time_unit :: size(16), rest :: binary>> = data
<<77, 84, 104, 100, 0, 0, 0, 6, 0, 1, 0, 20, 1, 224, 77, 84, 114, 107, 0, 0, 0,
36, 0, 255, 3, 3, 82, 101, 105, 0, 255, 81, 3, 7, 161, 32, 0, 255, 88, 4, 4,
2, 24, 8, 143, 143, 0, 255, 88, 4, ...>>
iex(3)> format
1
iex(4)> num_of_tracks
20
iex(5)> time_unit
480
フォーマット 1,トラック数 20,時間単位 480 のようです.MIDI ファイルのフォーマットは”0”, “1”, “2”の 3 種類が定義されています.フォーマット 1 はトラック構成を保存したフォーマットです (後にみていくと分かります).このファイルは 20 トラックの情報を含んでいます.時間単位 480 の意味は,四分音符の分解能が 480 という意味です.よくわからなくても気にせずに進みます.
ここでMidiParser
という module を定義しておきます.
defmodule MidiParser do
def parse_header(<<
"MThd",
6 :: size(32),
format :: size(16),
num_of_tracks :: size(16),
time_unit :: size(16),
_ :: binary>>) do
IO.puts "format: #{format}"
IO.puts "number of tracks: #{num_of_tracks}"
IO.puts "time unit: #{time_unit}"
end
end
トラック
トラックのチャンク構成は以下のようになっています.
<“MTrk”> <データ長 (4)> <データ本体> <…残りのトラックデータ>
MidiParser
に各トラックのデータ本体のバイナリを取得する関数を追加します.
defmodule MidiParser do
...
def parse_track(data) do
_parse_track_chunk(data, [])
end
defp _parse_track_chunk(<<>>, acc) do
Enum.reverse(acc)
end
defp _parse_track_chunk(<<
"MTrk",
length :: size(32),
body :: binary - size(length),
chunks :: binary>>, tracks) do
_parse_track_chunk(chunks, [body | tracks])
end
end
トラック部分のバイナリを渡して…
iex(7)> tracks = MidiParser.parse_track(chunks)
tracks
はトラックの情報を持った binary のリストです.長さを調べると…
iex(8)> length tracks
20
20 トラック分のバイナリを取得出来ました.これからトラック情報をパースしていきます.
<データ本体> は以下のような構成です.
<デルタタイム> <イベント> <デルタタイム> <イベント> <デルタタイム> <イベント>…
デルタタイムは可変長バイト列です.先頭 1 ビットはフラグで残りの 7 ビット分が数値を表しています.MSB(Most Significant Byte)が 1 のとき,次のバイトもデルタタイムを表現していることになります.この表現方法を解釈する関数を定義してみました.
def _extract_variable_length(<<msb :: 1, exp :: 7, _ :: binary>>) when msb == 0 do
<<exp :: 7>>
end
def _extract_variable_length(<<_ :: 1, exp :: 7, rest :: binary>>) do
next = _extract_variable_length(rest)
<< <<exp :: 7>> :: bitstring, next :: bitstring >>
end
これで数値部分の bitstring を抽出出来ました.バイト列に戻して値を抽出します.
def extract_variable_length(data) do
bits = _extract_variable_length(data)
bs = bit_size(bits)
padding_size = 8 - rem(bs, 8)
byte_data = << <<0 :: size(padding_size)>> :: bitstring, bits :: bitstring >>
bit_length = padding_size + bs
<<length :: size(bit_length)>> = byte_data
<<_ :: size(bit_length), chunks :: binary>> = data
{length, chunks}
end
あとはイベントを解釈するコードを追加するのですが,長くなりそうなので続きは後日. ソースコードはこちらに置きました.
(追記: 2017/09/18)
MIDI シーケンスに合わせてアニメーションする作品を公開しました.
https://sotahatakeyama.com/#/color-ball-dancing/