2019年8月20日火曜日

ProcessingのPythonモードでスペクトルアナライザーをつくる

ここ最近は『ジェネラティブ・アート』を読んでいました。Processingを使って偶然性に頼ったプログラムによるアートをしてみよう、という内容です。Processingだと簡単にビジュアルを生成できて楽しいですね。ツールの選び方として「作業だけに没頭できるか?」の観点は大事だな、と思いました。

今回はPythonで音楽ファイルを読み込んで再生時にアニメーションを表示する、スペクトルアナライザーを作ってみます。下の図みたいな動画でよく見かけるアレを作ります。『ジェネラティブ・アート』の前半くらいまでの内容と、オーディオを扱うためのライブラリー「Minim」の組み合わせで充分です。

ProcessingはJavaでコードを書くのが通常ですが、モードの切替えでPythonを使うこともできます。ただ、Pythonモードが実装されてから比較的に日が浅いからなのか、書籍もWebもPythonによるサンプルはあまり見かけません……。ライブラリーもサンプルはJavaだけが多いので、JavaのコードをPythonで読み替えていく作業が必要です(大したことではないけれど)。

コード

コード全体は下に載せてあるとおり。Minimは事前に追加されていることが前提です。音楽ファイルはProcessingのエディターにドロップしておく(プロジェクトファイルと同階層にフォルダーを作ってコピーされる)と、ファイル名だけを記述すれば良くなってラクです。

add_library('minim')

def setup() :
    # 初期化
    size(640, 480, P2D)
    background(0)
    smooth()
    
    # 音楽取込み
    global _minim
    global _player
    _minim = Minim(this)
    _player = _minim.loadFile('test.mp3')

    # 描画用変数
    global _width_pad    # 幅/y方向のウィンドウパディング
    global _height_pad   # 高/x方向のウィンドウパディング
    _width_pad = 20
    _height_pad = 80
    global _width_div    # 幅/y方向の分割数
    global _height_div   # 高/x方向の分割数
    _width_div = 30
    _height_div = 20
    
    global _cell_width   # セル幅
    global _cell_height  # セル高
    _cell_width = (width - _width_pad * 2) // _width_div
    _cell_height = (height - _height_pad * 2) // _height_div

    # 周波数解析
    #    10[Hz]〜サンプリングレートの1/2までをカウント
    global _fft
    global _log_band    # _width_divに応じた対数の帯域
    global _band_base   # 10[Hz]
    _fft = FFT(_player.bufferSize(), _player.sampleRate())
    _band_base = 10.0
    _log_band = pow((_player.sampleRate() / 2.0) / _band_base, 1.0 / _width_div)


def draw() :
    background(0)
    stroke(255)
    
    # セットアップ
    _fft.forward(_player.mix)
    translate(_width_pad, height - _height_pad)
    
    # 描画
    for x in range(_width_div) :
        # 列ごとの周波数帯域を計算して平均値を得る
        band_start = _band_base * pow(_log_band, x)
        band_end = band_start * _log_band
        spec_ave = _fft.calcAvg(band_start, band_end)
        
        # 対数[dB]に変換する
        signal = -100
        if spec_ave > 0.0 :
            signal = 20.0 * log(spec_ave/255) / log(10)
        
        # 高/x方向の分割数へマッピングする
        pile_num = map(signal, -100, 0, 0, _height_div)
        
        PileUpRect(_cell_width, _cell_height, 3, int(pile_num))
        translate(_cell_width, 0)


def PileUpRect(width, height, padding, quantity) :
    pushMatrix()
    # quantityの数だけセルを積む
    for q in range(quantity) :
        rect(padding, -padding, width - padding, padding-height)
        translate(0, -height)
    popMatrix()


def keyPressed() :
    # キーで再生オン/オフ
    if _player.isPlaying() == True :
        _player.pause()
    else :
        _player.play()