Processingでサウンドエフェクト処理をはじめよう

第3回 トーンコントロール [その1]

PC用のアンプ内蔵スピーカーには、TONEと書かれたツマミが付いているものもあります。少し高価なら、BASS、TREBLEと別々のツマミが付いているものまであります。 これらのツマミは、低音、高音のバランスを調整するもので、トーンコントロールと呼ばれています。
今回は、このトーンコントロールをProcessingを使ってプログラミングしてみましょう。
低音を調整するにはローシェルフ(Low Shelf)フィルタ、高音にはハイシェルフ(High Shelf)フィルタを使います。 これらは、バイクワッドと呼ばれるデジタルフィルタを使うことで実現できます。 デジタルフィルタの原理を理解するには難しい数学の知識が必要になりますが、 プログラミングで使ってみたいと言う場合には「Audio-EQ-cookbook」と言うありがたい設計ドキュメントがネット上に公開されています。(英語ですが…)
この設計ドキュメントは、Robert Bristow-Johnson氏が作者で、この記事の最後に参考文献としてURLを載せておきます。

今回は、この「Audio-EQ-cookbook」に従って、トーンコントロールを作成してみたいと思います。
以下がProcessingによるトーンコントロールのソースです。

[ToneControlタブのソース]

import ddf.minim.*;
import ddf.minim.effects.*;

float FS = 44100.0;
float LF = 200.0;
float LGAIN = 6.0;
float HF = 5000.0;
float HGAIN = 6.0;
float DGAIN = 0.5;

Minim minim;
AudioPlayer player;
DownGain down;
BassTone bass;
HiTone hi;

void setup()
{
  size(200, 200);
  minim = new Minim(this);
  player = minim.loadFile("sample.wav", 1024);
  down = new DownGain(DGAIN);
  bass = new BassTone(FS, LF, LGAIN);
  hi = new HiTone(FS, HF, HGAIN);
  player.addEffect(down);
  player.addEffect(bass);
  player.addEffect(hi);
  player.play();
}

void draw()
{
  background(0);
  stroke(255);
  for(int i = 0; i < player.left.size()-1; i++)
  {
    line(i, 50 + player.left.get(i)*50, i+1, 50 + player.left.get(i+1)*50);
    line(i, 150 + player.right.get(i)*50, i+1, 150 + player.right.get(i+1)*50);
  }

}

void stop()
{
  player.close();
  minim.stop();
  
  super.stop();
}

[BassToneタブのソース]

class BassTone implements AudioEffect
{
  float b0, b1, b2, a0, a1, a2;
  float[] lx = new float[2];
  float[] ly = new float[2];
  float[] rx = new float[2];
  float[] ry = new float[2];
  
  BassTone(float fs, float f, float g)
  {
    lx[0] = 0.0;
    lx[1] = 0.0;
    ly[0] = 0.0;
    ly[1] = 0.0;
    rx[0] = 0.0;
    rx[1] = 0.0;
    ry[0] = 0.0;
    ry[1] = 0.0;
    float omega = 2.0 * 3.141592 * ( f / fs );
    float sn = sin(omega);
    float cs = cos(omega);
    
    float fQ = 1.0 / 1.41421356;
    float A = pow(10.0, g / 40.0);
    float a = sn / (2.0 * fQ);
    float b = sqrt(A / fQ);
    
    b0 = A * ( ( A + 1.0 ) - ( A - 1.0 ) * cs + b * sn );
    b1 = 2.0 * A * ( ( A - 1.0 ) - ( A + 1.0 ) * cs );
    b2 = A * ( ( A + 1.0 ) - ( A - 1.0 ) * cs - b * sn );
    a0 = (( A + 1.0 ) + ( A - 1.0 ) * cs + b * sn );
    a1 = -2.0 * ( ( A - 1.0 ) + ( A + 1.0 ) * cs );
    a2 = (( A + 1.0 ) + ( A - 1.0 ) * cs - b * sn );
  }
  
  void biquad_process(float[] samp, float[] x, float[] y)
  {
    float[] out = new float[samp.length];
    out[0] = ( b0 / a0 ) * samp[0] +
      ( b1 / a0 ) * x[1] +
      ( b2 / a0 ) * x[0] -
      ( a1 / a0 ) * y[1] -
      ( a2 / a0 ) * y[0];
    
    out[1] = ( b0 / a0 ) * samp[1] +
      ( b1 / a0 ) * samp[0] +
      ( b2 / a0 ) * x[1] -
      ( a1 / a0 ) * out[0] -
      ( a2 / a0 ) * y[1];
    for ( int n = 2; n < samp.length; n++ )
    {
      out[n] = ( b0 / a0 ) * samp[n] +
        ( b1 / a0 ) * samp[n-1] +
        ( b2 / a0 ) * samp[n-2] -
        ( a1 / a0 ) * out[n-1] -
        ( a2 / a0 ) * out[n-2];
    }
    
    x[0] = samp[samp.length - 2];
    x[1] = samp[samp.length - 1];
    y[0] = out[out.length - 2];
    y[1] = out[out.length - 1];
    
    arraycopy(out, samp);
  }
    
  void process(float[] samp)
  {
    biquad_process(samp, lx, ly);
  }
  void process(float[] left, float[] right)
  {
    biquad_process(left, lx, ly);
    biquad_process(right, rx, ry);
  }
}

[HiToneタブのソース]

class HiTone implements AudioEffect
{
  float b0, b1, b2, a0, a1, a2;
  float[] lx = new float[2];
  float[] ly = new float[2];
  float[] rx = new float[2];
  float[] ry = new float[2];
  
  HiTone(float fs, float f, float g)
  {
    lx[0] = 0.0;
    lx[1] = 0.0;
    ly[0] = 0.0;
    ly[1] = 0.0;
    rx[0] = 0.0;
    rx[1] = 0.0;
    ry[0] = 0.0;
    ry[1] = 0.0;
    float omega = 2.0 * 3.141592 * ( f / fs );
    float sn = sin(omega);
    float cs = cos(omega);
    
    float fQ = 1.0 / 1.41421356;
    float A = pow(10.0, g / 40.0);
    float a = sn / (2.0 * fQ);
    float b = sqrt(A / fQ);
    
    b0 = A * ( ( A + 1.0 ) + ( A - 1.0 ) * cs + b * sn );
    b1 = -2.0 * A * ( ( A - 1.0 ) + ( A + 1.0 ) * cs );
    b2 = A * ( ( A + 1.0 ) + ( A - 1.0 ) * cs - b * sn );
    a0 = ( A + 1.0 ) - ( A - 1.0 ) * cs + b * sn;
    a1 = 2.0 * ( ( A - 1.0 ) - ( A + 1.0 ) * cs );
    a2 = ( A + 1.0 ) - ( A - 1.0 ) * cs - b * sn;
  }
  
  void biquad_process(float[] samp, float[] x, float[] y)
  {
    float[] out = new float[samp.length];
    out[0] = ( b0 / a0 ) * samp[0] +
      ( b1 / a0 ) * x[1] +
      ( b2 / a0 ) * x[0] -
      ( a1 / a0 ) * y[1] -
      ( a2 / a0 ) * y[0];
    
    out[1] = ( b0 / a0 ) * samp[1] +
      ( b1 / a0 ) * samp[0] +
      ( b2 / a0 ) * x[1] -
      ( a1 / a0 ) * out[0] -
      ( a2 / a0 ) * y[1];
    for ( int n = 2; n < samp.length; n++ )
    {
      out[n] = ( b0 / a0 ) * samp[n] +
        ( b1 / a0 ) * samp[n-1] +
        ( b2 / a0 ) * samp[n-2] -
        ( a1 / a0 ) * out[n-1] -
        ( a2 / a0 ) * out[n-2];
    }
    
    x[0] = samp[samp.length - 2];
    x[1] = samp[samp.length - 1];
    y[0] = out[out.length - 2];
    y[1] = out[out.length - 1];
    
    arraycopy(out, samp);
  }
    
  void process(float[] samp)
  {
    biquad_process(samp, lx, ly);
  }
  void process(float[] left, float[] right)
  {
    biquad_process(left, lx, ly);
    biquad_process(right, rx, ry);
  }
}

[DownGainタブのソース]

class DownGain implements AudioEffect
{
  float gain = 1.0;
  
  DownGain(float g)
  {
    gain = g;
  }
  
  void process(float[] samp)
  {
    float[] out = new float[samp.length];
    for ( int i = 0; i < samp.length; i++ )
    {
      out[i] = samp[i] * gain;
    }
    arraycopy(out, samp);
  }
  
  void process(float[] left, float[] right)
  {
    process(left);
    process(right);
  }
}

ToneControlは[File]-[New]で開いたエディタに入力、Save時にToneControlで保存し、BaseTone, HiTone, DownGainは[New Tab]で開いたエディタ上にそれぞれ入力してください。 (できあがったToneControlフォルダの中にdataフォルダを作って、その中にsample.wavファイルを入れることを忘れずに)

① パラメータの設定
ToneControlのソースの最初の方に、

float FS = 44100.0;
float LF = 200.0;
float LGAIN = 6.0;
float HF = 5000.0;
float HGAIN = 6.0;
float DGAIN = 0.5;

とあると思います。

float FS = 44100.0;

は、sample.wavのサンプリング周波数です。これがあっていないと正常にトーンコントロールができないので、再生したいWAVファイルのサンプリング周波数を知っている必要があります。 確実なのは44100HzのWAVファイルを自分で作ってしまう方法です。
iTunesを使った例を挙げます。

[iTunesで44100HzのWAVファイルを作る方法]

  1. メニューから[編集]-[設定」を選択する
  2. [一般]タブの[インポート設定]ボタンを押す
  3. インポート方法を「WAVエンコーダ」にする
  4. 設定から[カスタム...]を選ぶ
  5. サンプルレートから「44,100Hz」を選ぶ(その他は「自動」でOK)
  6. [OK]ボタンを3回押して、今まで開いたダイアログを全て閉じる
  7. iTunesでお好きな曲をインポートする
  8. iTunesMediaフォルダからインポートしたWAVファイルを探す
  9. そのWAVファイルをProcessingのToneControlフィルだの下のdataフォルダにコピーして、sample.wavにリネーム

この後、インポート設定を元のインポート方法に戻して置くことを忘れないようにしてください。そうしないと、ずっとWAVでインポートされてしまいます。

float LF = 200.0;

は、設定した数値以下の低音が調整できます。200.0は200Hz以下を増減できるようになります。

float LGAIN = 6.0;

は、低音の増減量で、単位はdB(デシベル)です。6dBで元の音の倍の音量になります。(200Hz以下が2倍の音量になるということ)
だんだんこつが分かってきましたね。

float HF = 5000.0;
float HGAIN = 6.0;

は、HFに設定した数値以上の高音がHGAINで調整できます。高音を減らしたい場合は、-2.0などマイナスの数値を入れます。 LGAINやHGAINは+12~-12の範囲で調整すると良いでしょう。
LF, HFはマイナスの値は指定できません。 また、サンプリング周波数の半分以上の数値も指定しない方が良いでしょう。(理由は割愛させてください)

最後に、

float DGAIN = 0.5;

ですが、これは音を歪ませないためにあります。 LGAINやHGAINでプラスの値を指定すると、データで表現できる範囲を超えてオーバーフローしてしまう場合があります。 音が歪むようなら、DGAINの値を1.0以下に設定してください。-6.0dB下げたい場合は、1/2 = 0.5にすれば0Kです。

ここからソースの説明と行きたいところですが、記事が長くなりそうですので、今回はここで一旦一区切りとします。

参考文献:
Robert Bristow-Johnson,
'Cookbook formulae for audio EQ biquad filter coefficients',
http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt

「Processingでサウンドエフェクト処理をはじめよう」一覧に戻る