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

第7回 ビブラート

今回は「ビブラート」です。
トーン」は音を補正するタイプのエフェクトで、「エコー」は遅延音を加えるタイプのエフェクトでした。
「ビブラート」は音を変調するタイプのエフェクトです。 どういう変調かと言うと、指定した周期で原音の周波数をずらします。 周波数を変調するので周波数変調と呼ばれ、これを英語にするとFrequency Modulation になります。略してFMです。
どこかで聞いたことがありますね。 そう、ラジオのFMです。
FMラジオは、各局に割り当てられた周波数の搬送波をサウンドの波形を元に周波数変調して送っているのですね。 ラジオの場合は数十MHzの高い周波数ですから、音として聞こえるものではないですが(音を鳴らすときは、変調信号だけを取り出しています)、ビブラートでは可聴帯域の音声に数Hzの変調をかけるので、そのまま耳に聞こえます。 FMラジオと基本的に原理は一緒です。
前置きはこのくらいにして、「ビブラート」処理のProcessingのソースを説明します。

[Vibratoタブのソース]
import ddf.minim.*;
import ddf.minim.effects.*;

// parameters
float DEPTH = 0.002;    // sec
float FREQUENCY = 5.0;  // Hz

float FS = 44100.0;

Minim minim;
AudioPlayer player;
VibratoClass vibrato;

void setup()
{
  size(200, 200);
  minim = new Minim(this);
  player = minim.loadFile("sample.wav", 1024);
  vibrato = new VibratoClass(FS, DEPTH, FREQUENCY);
  player.addEffect(vibrato);
  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();
}
[VibratoClassタブのソース]
class VibratoClass implements AudioEffect
{
  float[] l_buffer;
  float[] r_buffer;
  int buffer_size;  
  int l_total_index, r_total_index;
  float dt;      // dt >= depth
  float depth;
  float freq;
  float fs;
  
  VibratoClass(float f, float dp, float fq)
  {
    fs = f;
    depth = fs * dp;
    dt = depth;
    freq = fq;
    
    buffer_size = (int)(2.0 * dt) + 256;
        
    l_buffer = new float[buffer_size];
    r_buffer = new float[buffer_size];
    
    l_total_index = 0;
    r_total_index = 0;
    
    for (int i = 0; i < buffer_size; i++)
    {
      l_buffer[i] = 0.0;
      r_buffer[i] = 0.0;
    }
  }
  
  int vibrato_process(float[] samp, float[] buffer, int ix)
  {
    float[] out = new float[samp.length];
    int n = ix;
    for ( int i = 0; i < samp.length; i++ )
    {
      int index = n % buffer_size;
      buffer[index] = samp[i];
      float fmod = dt + depth * sin( 2.0 * PI * freq * n / fs );
      float t = (float)index - fmod;
      int m0 = (int)t;
      int m1 = m0 + 1;
      float delta = t - (float)m0;
      if (m0 < 0)
      {
        m0 += buffer_size;
      }
      if (m1 < 0)
      {
        m1 += buffer_size;
      }
      out[i] = delta * buffer[m1] + (1.0 - delta) * buffer[m0];
      n++;
    }    
    arraycopy(out, samp);
    
    return n;
  }
  
  void process(float[] samp)
  {
    l_total_index = vibrato_process(samp, l_buffer, l_total_index);
  }
  
  void process(float[] left, float[] right)
  {
    l_total_index = vibrato_process(left, l_buffer, l_total_index);
    r_total_index = vibrato_process(right, r_buffer, r_total_index);
  }
}

① Vibratoタブ(メイン処理)のソースの説明

以下が、ビブラート処理のパラメータです。

float DEPTH = 0.002;    // sec
float FREQUENCY = 5.0;  // Hz

DEPTHはビブラート効果のかかり具合(強さ、深さ)です。
今回のサンプルの場合は、秒で指定します。 これを大きくすると効果は強くなりますが、あまり大きくすると掛かりすぎて、元の音がなんだかわからなくなります。まずは、2msec(=0.002sec)あたりにしておきます。
FREQUENCYは周波数変調をかける周波数です。この数値を大きくすると、変調のうねりが速くなります。

ところで、

// sec

とありますが、これは「コメント」というもので、説明などを書き留めておくものです。
// 以降の文字は、プログラム動作には影響を与えず無視されます。
ソースコード上に適切なコメントを入れておくと、プログラムがわかりやすくなります。 他の人に見てもらうときなどは便利ですね。
ところが、Processingには少し困ったことがあります。 Processingは、原則、2バイトコード(全角文字など)に対応していないのです。 なので、コメントに日本語をいえると、プログラムが誤動作することがあります。 2バイトコードのコメントを入れると、コメントして判断してくれないことがあるんでしょうね。
2バイトコードには、UnicodeやShift-JISなどいろいろなタイプがあり、どれで書き込んだかによって動作が異なるので、やっかいです。
この記事で扱う程度のソースコードの長さだと「コメントなしでも良いかな?」と思い、あまりコメントを入れないようにしています。
英語のコメントなら誤動作は無いですが、私の変な英語だとかえって混乱しますしね。

vibrato = new VibratoClass(FS, DEPTH, FREQUENCY);

で、VibratoClassのオブジェクトvibratoを生成して、

player.addEffect(vibrato);

で、playerオブジェクトにエフェクトを追加しているところは、他のエフェクト処理と同じです。

② VibratoClassタブ(エフェクト処理)のソースの説明

VibratoClassのコンストラクタで、変数の初期化をしています。

サンプリング周波数の設定

fs = f;

エフェクトの深さの設定

depth = fs * dp;

周波数を揺らすためには、エコーで使った遅延の仕組みを利用します。 遅延音を読み出す時間間隔を広くすれば、低い音への変調され、間隔を狭めれば高い音に変調されます。
今回のサンプルでは、その間隔の計算にSIN関数を使っています。 SIN関数の振幅の強さが、処理の効きの強さ(深さ)に相当し、depthはその強さを指定しています。
SIN関数は-1.0~+1.0の間の値になるので、それにdepthをかけると、-depth~+depthの間の値になります。depthを2msecにすると、時間間隔は-2msec~+2msecの間で振れるわけですね。 2msecはデータの個数で言うと、サンプリング周波数 × 2msec前のデータになります。
コンストラクタに渡されたDEPTHの値にfsをかけて、時間をデータの個数に変換しています。

変調のための遅延値

dt = depth;

時間間隔が-2msec~+2msecということは、-で前のデータ、+で先のデータになります。 今再生しているより先のデータはまだ読み込まれていないので、その分、前のデータを使って変調処理をします。
つまり、depthの分だけ前のデータを中心として変調すれば、今のデータより先のデータが必要になることはないですよね。
dtにはその遅延値(どれだけ遡るか)を入れます。 今のデータを超さないようにするので、dt >= depth という関係になります。

周波数変調の周期の設定

freq = fq;

バッファサイズの設定

buffer_size = (int)(2.0 * dt) + 256;

どこまで前のデータが必要かと言うと、dt + depthになります。
dt = depth としているので、dtの2倍のバッファが必要になります。
念のため、256をプラスしているのは、エコーの時同様小心者だからです。

process()メソッドは今までのエフェクトとあまり変わらないので、
vibrato_process()メソッドを説明します。

int n = ix;

process()メソッドを呼び出すとき、LチャンネルかRチャンネルのどちらかの「現在のサウンドデータが何番目」かを示すインデックスを引数ixに代入しています。 これは、SIN関数で変調をかけるときに、最初から連続で何番目のデータなのか、が必要になるためです。 nixを代入して、メソッド内部の処理でインクリメントした後に、return nで返します。

for ( int i = 0; i < samp.length; i++ )

ストリームのバッファサイズだけ処理を繰り返します。

int index = n % buffer_size;

nはサウンドデータの最初からの位置なので、現在のストリームのバッファの何番目になるかの変換をしています。
%
は割り算の余りを求める演算子で、例えば、buffer_sizeが1024で、最初から1025番目のデータは、余りが1なので、バッファ内では2番目のデータになります。 一番最初は0なので、1は2番目になります。

buffer[index] = samp[i];

今のサウンドデータを遅延用のバッファの現在地に入れています。エコーの時に説明をしたリングバッファですね。

float fmod = dt + depth * sin( 2.0 * PI * freq * n / fs );

上記の計算で変調度を計算します。この式の考え方は「エフェクトの深さの設定」のところで少し触れた内容です。

float t = (float)index - fmod;

この変調度は時間軸での変調に相当するので、現在の位置からどれだけ前かという計算になります。
tが実数なのは、変調の計算結果が整数ではないためです。
「どのくらい前」を表すtが実数だと、サンプリング周波数間隔で並ぶデータの間の時間を指すことになります。 そのため、後で、その前後のデータから線形補間を使って、その時間に相当するデータの値を計算しています。

int m0 = (int)t; int m1 = m0 + 1;

tを切り捨てて前後のデータの前のデータを指すインデックスを計算し、それに+1してその次のデータを指すインデックスを計算しています。

float delta = t - (float)m0;

tから前のデータの数字をひくことで、前のデータと次のデータの間隔の中のどの位置を指しているかを求めています。 実数なので、小数点以下の数字ですね。

if (m0 < 0)
{
  m0 += buffer_size;
}
if (m1 < 0)
{
  m1 += buffer_size;
}

リングバッファを使っていますので、前に遡る時にマイナスになる場合があり、buffer_sizeを足しています。 これもエコーの時と同じ処理です。

out[i] = delta * buffer[m1] + (1.0 - delta) * buffer[m0];

線形補間をしています。 例えば、deltaが0.5だったら、m1の位置のデータ値とm0の位置のデータ値のそれぞれ半分を足していることになります。 0.5だとちょうど真ん中なので、感覚的にもイメージできるかと思います。 0.3とかの場合も少し考えると理解できるかと思います。

n++;

現在のインデックス位置をインクリメントして、for文の繰り返しです。

return n;

最後に、インデクス位置を返します。 ビブラートは時間軸方向の変調なので、少し分かりづらかったかも知れません。 実際にプログラムを走らせて、エフェクト効果を聞いてみてください。 PDEファイルが保存されたフォルダにdataフォルダを作って、皆さんが用意されたsample.wav(44.1kHz)を置くことをお忘れなく。

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