35
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-08-02

バイナリに対するパターンマッチでバイナリを分解

Elixir のパターンマッチはバイナリデータに対して利用することが可能で、実際、なかなか実用的である。

例えば 4バイトずつ整数と文字列が連続して埋まっているバイナリがあったとすると

<<size::unsigned-integer-size(32), id::bitstring-size(32)>> = some_data

というパターンマッチを書くと、sizeid という変数にそれぞれの値が束縛される。Ruby だと unpack を使ってその辺のバイナリを弄ったりするが、それよりもずっと簡潔且つ宣言的に記述できるのが良い。

AIFF のバイナリを Elixir でいじる

とはいえ、なかなかバイナリをいじる機会はないのでこの折角の機能を試す機会がなかったのだが、つい先日たまたま RubyでAIFFファイルをいじるスクリプト書いた という AIFF ファイルを Ruby で弄ってる記事を見かけた。

なるほど、カッとなったので、これを題材に Elixir でもやってみよう、というのが本エントリである。

AIFF の仕様

AIFF の仕様は先のエントリにある図を見ればすぐわかる。

この辺も詳しい。

それほど複雑な構造にはなっていなくて、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 の入れ子構造になってるのでもうちょっと複雑だ。

全体のコード

というわけで、以下がコード全体である。

iroha.ex
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
35
36
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
35
36

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?