バイナリに対するパターンマッチでバイナリを分解
Elixir のパターンマッチはバイナリデータに対して利用することが可能で、実際、なかなか実用的である。
例えば 4バイトずつ整数と文字列が連続して埋まっているバイナリがあったとすると
<<size::unsigned-integer-size(32), id::bitstring-size(32)>> = some_data
というパターンマッチを書くと、size
と id
という変数にそれぞれの値が束縛される。Ruby だと unpack
を使ってその辺のバイナリを弄ったりするが、それよりもずっと簡潔且つ宣言的に記述できるのが良い。
AIFF のバイナリを Elixir でいじる
とはいえ、なかなかバイナリをいじる機会はないのでこの折角の機能を試す機会がなかったのだが、つい先日たまたま RubyでAIFFファイルをいじるスクリプト書いた という AIFF ファイルを Ruby で弄ってる記事を見かけた。
なるほど、カッとなったので、これを題材に Elixir でもやってみよう、というのが本エントリである。
AIFF の仕様
AIFF の仕様は先のエントリにある図を見ればすぐわかる。
- GO!GO!neko819 AIFFファイルのバイナリを見る
- http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/AIFF/Docs/AIFF-1.3.pdf
この辺も詳しい。
それほど複雑な構造にはなっていなくて、Form、Common、Soud Data という 3つのチャンクがあり、それぞれのチャンクの中にサイズとかサンプルレートとかその辺のメタデータが埋め込まれていて、最後に音声データが続いているという感じである。
Elixir で解析する
Elixir で解析するには AIFF ファイルを読み込んだ後、冒頭に書いたようにパターンマッチで分解していけばよい。
例えば AIFF ファイル全体が data
に入ってたとすると
<<
id :: bitstring-size(32),
size :: unsigned-integer-size(32),
type :: bitstring-size(32)
chunks :: binary
>> = data
というパターンマッチで、Form の ckID、ckSize、formType が得られる。また、それ以降の部分は chunks
に束縛される。簡単だ。同様の方針で仕様に従ってモリモリ書いていけば OK である。
工夫
ただ、AIFF の中身を分解して変数に束縛して終わり、だとあまりに面白くない。そこで、Form、Common、Soud Data それぞれに対応する構造体を作ってやって、ファイルを読み取った結果をきちんとデータ構造に構造化し、プログラムから利用しやすいようにしてやる。
なお、Elixir における構造体は、値の保証付きマップのようなものである。キーと値の構造をマップで表現した場合、フィールドの存在有無は実行時にしか保証されないが、構造体で明示的にフィールドを宣言してやることで、コンパイル時の保証が得られる。
例えば Form の Chunk は
defmodule FormChunk do
defstruct [:id, :size, :type, :common, :sound_data]
def new(
<<
id :: bitstring-size(32),
size :: unsigned-integer-size(32),
type :: bitstring-size(32),
chunks :: binary,
>>) do
%FormChunk{id: id, size: size, type: type, chunks: chunks}
end
end
こんな感じで定義して
{:ok, file} = File.open path, [:read]
IO.inspect FormChunk.new IO.binread(file, :all)
とすると FormChunk 構造体が得られる。見てのとおりバイナリデータへのパターンマッチと変数への束縛は関数の引数でやっている。
実際には Common と Sound Data は Form の入れ子構造になってるのでもうちょっと複雑だ。
全体のコード
というわけで、以下がコード全体である。
defmodule FormChunk do
defstruct [:id, :size, :type, :common, :sound_data]
def new(
<<
id :: bitstring-size(32),
size :: unsigned-integer-size(32),
type :: bitstring-size(32),
chunks :: binary,
>>) do
## Common Chunk のメタデータを抽出
<<
common_id :: unsigned-integer-size(32),
common_chunk_size :: unsigned-integer-size(32),
remain :: binary
>> = chunks
## 抽出された値で Common Chunk と Sound Data Chunk を分ける
<<
common_data :: binary-size(common_chunk_size),
sound_data_chunk :: binary
>> = remain
%FormChunk{
id: id,
size: size,
type: type,
common: CommonChunk.new(common_id, common_chunk_size, common_data),
sound_data: SoundDataChunk.new(sound_data_chunk)
}
end
end
defmodule CommonChunk do
defstruct [
:id,
:size,
:num_channels,
:num_sample_frames,
:sample_size,
:sample_rate,
]
def new(
id,
size,
<<
num_channels :: unsigned-integer-size(16),
num_sample_frames :: unsigned-integer-size(32),
sample_size :: unsigned-integer-size(16),
# FIXME: Sound Rate よく分からん
_x :: unsigned-integer-size(16), # ?
sample_rate :: unsigned-integer-size(16), # hmm...
_y :: binary
>>) do
%CommonChunk{
id: id,
size: size,
num_channels: num_channels,
num_sample_frames: num_sample_frames,
sample_size: sample_size,
sample_rate: sample_rate,
}
end
end
defmodule SoundDataChunk do
defstruct [
:id,
:size,
:offset,
:block_size,
:sound_data,
]
def new(
<<
id :: bitstring-size(32),
size :: unsigned-integer-size(32),
offset :: unsigned-integer-size(32),
block_size :: unsigned-integer-size(32),
sound_data :: binary
>>) do
%SoundDataChunk{
id: id,
size: size,
offset: offset,
block_size: block_size,
sound_data: sound_data,
}
end
end
[path] = System.argv
{:ok, file} = File.open path, [:read]
IO.inspect FormChunk.new IO.binread(file, :all)
File.close file
元エントリに同じく Sound Rate 周りの解釈がよく分からなかったが、まあそこは趣旨ではないので目をつむっておこう。
適当なファイルを食わせてみよう。
$ elixir iroha.exs /System/Library/Sounds/Basso.aiff
%FormChunk{common: %CommonChunk{id: 1129270605, num_channels: 2,
num_sample_frames: 13704, sample_rate: 44100, sample_size: 24, size: 18},
id: "FORM", size: 82270,
sound_data: %SoundDataChunk{block_size: 0, id: "SSND", offset: 0, size: 82232,
sound_data: <<0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ...>>},
type: "AIFF"}
用意した構造体の中にきちんとデータが埋まった。万歳。
追記
コメントにも書いたが元のコードの FormChunk で Common や Sound Data にデータを分ける辺りが冗長だったので、コンパクトにした。
defmodule FormChunk do
defstruct [:id, :size, :type, :common, :sound_data]
def new(
<<
id :: bitstring-size(32),
size :: unsigned-integer-size(32),
type :: bitstring-size(32),
common_id :: unsigned-integer-size(32),
common_chunk_size :: unsigned-integer-size(32),
common_data :: binary-size(common_chunk_size),
sound_data_chunk :: binary
>>) do
%FormChunk{
id: id,
size: size,
type: type,
common: CommonChunk.new(common_id, common_chunk_size, common_data),
sound_data: SoundDataChunk.new(sound_data_chunk)
}
end
end