エフェクト、周波数解析

      エフェクト、周波数解析 はコメントを受け付けていません

前回は、Minimライブラリを使って音声ファイルを再生し、波形を画面に表示しました。今回は、音の中に含まれる周波数を扱います。周波数を使うと、低い音だけに反応させたり、高い音に合わせて図形を変化させたりすることができます。

音楽や声は、ひとつの単純な音ではなく、さまざまな周波数の音が混ざったものです。このページでは、フィルタで周波数を変化させる方法と、FFTを使って周波数を解析する方法を扱います。

map()

まず、音のプログラムでもよく使うmap()を確認します。map()は、ある範囲の値を、別の範囲の値に変換するための関数です。

map(value, low1, high1, low2, high2)

それぞれの意味は次の通りです。

項目意味
value変換したい値
low1変換前の範囲の最小値
high1変換前の範囲の最大値
low2変換後の範囲の最小値
high2変換後の範囲の最大値

たとえば、mouseXは画面の左端で0、右端でwidthになります。この値を100〜300の範囲に変換すると、マウスの動きを別の範囲に置き換えることができます。

リスト1
void setup() {
  size(400, 200);
}

void draw() {
  background(255);

  // 円が動く範囲を線で表示する
  // この線の左端が100、右端が300
  stroke(0);
  line(100, height / 2, 300, height / 2);

  // mouseXは0〜widthの範囲で変化する
  // それを100〜300の範囲に変換する
  float x = map(mouseX, 0, width, 100, 300);

  // 変換したx座標に円を描く
  noStroke();
  fill(255, 0, 0);
  ellipse(x, height / 2, 20, 20);

  // 現在の値を画面に表示する
  fill(0);
  text("mouseX: " + mouseX, 20, 30);
  text("mapped x: " + int(x), 20, 50);
}

ローパスフィルタ、ハイパスフィルタ

ここから音の周波数を直接コントロールします。代表的なものに、特定の周波数を通過させたり、カットしたりするフィルタがあります。

ローパスフィルタは低い周波数を通し、高い周波数を弱くします。ハイパスフィルタは高い周波数を通し、低い周波数を弱くします。ここでは、この2つを試します。

ローパスフィルタ

次のサンプルでは、マウスの横位置でローパスフィルタの周波数を変えます。左に動かすほど高い音が弱くなり、右に動かすほど高い音も通りやすくなります。

リスト2
import ddf.minim.*;
import ddf.minim.effects.*;

Minim minim;
AudioPlayer player;
LowPassSP lowPass;  // ローパスフィルタを扱うための変数

float cutoff = 5000;  // フィルタの基準になる周波数
float waveHeight = 50;

void setup() {
  size(512, 220);

  minim = new Minim(this);

  // sample.mp3を読み込み、ループ再生する
  player = minim.loadFile("sample.mp3", 1024);
  player.loop();

  // 最初は5000Hzでローパスフィルタを作る
  // player.sampleRate()は、音声ファイルのサンプリングレート
  lowPass = new LowPassSP(cutoff, player.sampleRate());

  // 音声にフィルタを追加する
  player.addEffect(lowPass);
}

void draw() {
  background(0);

  // mouseXを20〜5000Hzに変換する
  // 左端では20Hz、右端では5000Hzになる
  cutoff = map(mouseX, 0, width, 20, 5000);

  // 変換した周波数をローパスフィルタに設定する
  lowPass.setFreq(cutoff);

  // 左右の波形の中心線を描く
  stroke(80);
  line(0, 70, width, 70);
  line(0, 160, width, 160);

  // 音声ファイルの波形を白で描く
  stroke(255);

  for (int i = 0; i < player.bufferSize() - 1; i++) {
    // i番目とi+1番目の音声データを横位置にする
    float x1 = map(i, 0, player.bufferSize() - 1, 0, width);
    float x2 = map(i + 1, 0, player.bufferSize() - 1, 0, width);

    // leftとrightの値は-1.0〜1.0くらい
    // waveHeightを掛けることで、波形を見やすくする
    float leftY1 = 70 + player.left.get(i) * waveHeight;
    float leftY2 = 70 + player.left.get(i + 1) * waveHeight;

    float rightY1 = 160 + player.right.get(i) * waveHeight;
    float rightY2 = 160 + player.right.get(i + 1) * waveHeight;

    line(x1, leftY1, x2, leftY2);
    line(x1, rightY1, x2, rightY2);
  }

  // 現在のフィルタ周波数を表示する
  fill(255);
  text("Low Pass Filter", 10, 20);
  text("cutoff: " + int(cutoff) + " Hz", 10, 40);
}

void mousePressed() {
  // マウスを押すと、現在の周波数をコンソールに表示する
  println(cutoff);
}

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

ハイパスフィルタ

次はハイパスフィルタです。マウスを右に動かすほど、低い音がカットされやすくなります。

リスト3
import ddf.minim.*;
import ddf.minim.effects.*;

Minim minim;
AudioPlayer player;
HighPassSP highPass;  // ハイパスフィルタを扱うための変数

float cutoff = 100;
float waveHeight = 50;

void setup() {
  size(512, 220);

  minim = new Minim(this);

  player = minim.loadFile("sample.mp3", 1024);
  player.loop();

  // 最初は100Hzでハイパスフィルタを作る
  highPass = new HighPassSP(cutoff, player.sampleRate());

  // 音声にフィルタを追加する
  player.addEffect(highPass);
}

void draw() {
  background(0);

  // mouseXを100〜10000Hzに変換する
  // 右に行くほど、低い音がカットされやすくなる
  cutoff = map(mouseX, 0, width, 100, 10000);

  // 変換した周波数をハイパスフィルタに設定する
  highPass.setFreq(cutoff);

  // 左右の波形の中心線を描く
  stroke(80);
  line(0, 70, width, 70);
  line(0, 160, width, 160);

  // 音声ファイルの波形を白で描く
  stroke(255);

  for (int i = 0; i < player.bufferSize() - 1; i++) {
    float x1 = map(i, 0, player.bufferSize() - 1, 0, width);
    float x2 = map(i + 1, 0, player.bufferSize() - 1, 0, width);

    float leftY1 = 70 + player.left.get(i) * waveHeight;
    float leftY2 = 70 + player.left.get(i + 1) * waveHeight;

    float rightY1 = 160 + player.right.get(i) * waveHeight;
    float rightY2 = 160 + player.right.get(i + 1) * waveHeight;

    line(x1, leftY1, x2, leftY2);
    line(x1, rightY1, x2, rightY2);
  }

  fill(255);
  text("High Pass Filter", 10, 20);
  text("cutoff: " + int(cutoff) + " Hz", 10, 40);
}

void mousePressed() {
  println(cutoff);
}

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

フィルタは、特定の周波数を通したり弱めたりするための処理です。ローパスは低い音を通し、ハイパスは高い音を通す、と考えると理解しやすくなります。

FFT

FFTは、音の中にどの周波数がどれくらい含まれているかを調べるための方法です。正式にはFast Fourier Transform、高速フーリエ変換と呼ばれます。数式を直接書かなくても、MinimのFFTを使うことで周波数の情報を取り出せます。

詳しい原理を知りたい場合は、以下のページも参考になります。
FFTの説明

サウンドデータダウンロード
frequency.aif

各種サイン波のダウンロード(学内用)
sinewave.zip

リスト4
import ddf.minim.*;
import ddf.minim.analysis.*;

Minim minim;
AudioPlayer player;
FFT fft;  // 周波数解析を行うための変数

void setup() {
  size(1024, 240);

  minim = new Minim(this);

  // frequency.aifをdataフォルダに入れておく
  // 第2引数の1024は、解析に使うバッファサイズ
  player = minim.loadFile("frequency.aif", 1024);
  player.loop();

  // FFTオブジェクトを作成する
  // player.bufferSize()は1024
  // player.sampleRate()は多くの場合44100Hz
  fft = new FFT(player.bufferSize(), player.sampleRate());

  println("sample rate: " + player.sampleRate());
  println("spec size: " + fft.specSize());
  println("bandwidth: " + fft.getBandWidth());
}

void draw() {
  background(255);

  // player.mixは左右の音を混ぜた音声データ
  // fft.forward()で、その音声データを周波数成分に変換する
  fft.forward(player.mix);

  // FFTの結果を線で表示する
  // 左側が低い周波数、右側が高い周波数
  stroke(0);

  for (int i = 0; i < fft.specSize(); i++) {
    // i番目の周波数帯域の強さを取得する
    float band = fft.getBand(i);

    // iの値を画面の横位置に変換する
    float x = map(i, 0, fft.specSize() - 1, 0, width);

    // 音の強さを見やすくするため、少し大きくして描く
    float lineHeight = band * 4;

    // 画面の下から上に伸びる線を描く
    line(x, height, x, height - lineHeight);
  }

  fill(0);
  text("FFT Spectrum", 10, 20);
  text("bandwidth: " + nf(fft.getBandWidth(), 1, 2) + " Hz", 10, 40);
}

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

サンプリングレートが44100Hz、バッファサイズが1024の場合、1つの周波数帯域の幅は約43.07Hzになります。つまり、fft.getBand(23)は、およそ990Hz付近の強さを表します。

特定の周波数に反応させる

次のサンプルでは、1000Hz、5000Hz、10000Hz付近の音に円の大きさを反応させます。どの周波数が何番目の帯域に入るかを確認するために、setup()内で帯域の一覧をコンソールに出力しています。

1000Hz付近は23番目の帯域に含まれるため、円の大きさに使う場合は次のように書けます。

ellipse(100, 100, fft.getBand(23), fft.getBand(23));
リスト5
import ddf.minim.*;
import ddf.minim.analysis.*;

Minim minim;
AudioPlayer player;
FFT fft;

void setup() {
  size(400, 220);
  noStroke();
  textFont(createFont("Arial", 14));

  minim = new Minim(this);

  // sin01000.AIFをdataフォルダに入れておく
  // ここでは1000Hzの音を再生する
  player = minim.loadFile("sin01000.AIF", 1024);
  player.loop();

  fft = new FFT(player.bufferSize(), player.sampleRate());

  println("sample rate: " + player.sampleRate());
  println("spec size: " + fft.specSize());
  println("bandwidth: " + fft.getBandWidth());

  // 各帯域が何Hzから何Hzくらいまでを表すか確認する
  for (int i = 0; i < fft.specSize(); i++) {
    println(i + " = " + fft.getBandWidth() * i + " ~ " + fft.getBandWidth() * (i + 1));
  }
}

void draw() {
  background(0);

  // 現在再生中の音を周波数解析する
  fft.forward(player.mix);

  // それぞれの周波数帯域の強さを取り出す
  float level1000 = fft.getBand(23);   // 約1000Hz
  float level5000 = fft.getBand(116);  // 約5000Hz
  float level10000 = fft.getBand(232); // 約10000Hz

  // 値が小さいと見えにくいので、円の大きさとして少し大きくする
  float size1000 = level1000 * 2;
  float size5000 = level5000 * 2;
  float size10000 = level10000 * 2;

  // 周波数の強さに応じて円を描く
  fill(255);
  ellipse(100, 110, size1000, size1000);
  ellipse(200, 110, size5000, size5000);
  ellipse(300, 110, size10000, size10000);

  // ラベルを描く
  fill(255, 0, 0);
  textAlign(CENTER, CENTER);
  text("1000Hz", 100, 110);
  text("5000Hz", 200, 110);
  text("10000Hz", 300, 110);
}

動きを滑らかにする

FFTの値をそのまま円の大きさに使うと、動きが細かく震えることがあります。そこで、現在の値を目標値に少しずつ近づけることで、動きを滑らかにします。

リスト6
float objY = 0;     // 現在の四角形の高さ
float targetY = 0;  // 目標の高さ
float distanceY;    // 現在地と目標値の差
float delay = 10.0; // 値が変化する遅さ

void setup() {
  size(400, 400);
  noStroke();
}

void draw() {
  background(255);

  // マウスを押している間は、目標値をmouseYにする
  if (mousePressed) {
    targetY = mouseY;
  } else {
    targetY = 0;
  }

  // 目標値と現在値の差を求める
  distanceY = targetY - objY;

  // 差を一気に足さず、delayで割った分だけ足す
  // これにより、objYがtargetYへ少しずつ近づく
  objY = objY + distanceY / delay;

  // objYの高さで四角形を描く
  fill(0);
  rect(0, 0, width, objY);
}

この考え方をFFTに使うと、音に反応する図形の動きが滑らかになります。

リスト7
import ddf.minim.*;
import ddf.minim.analysis.*;

Minim minim;
AudioPlayer player;
FFT fft;

float obj1 = 0;
float obj2 = 0;
float obj3 = 0;

float target1;
float target2;
float target3;

float distance1;
float distance2;
float distance3;

float delay = 10.0;

void setup() {
  size(400, 220);
  noStroke();
  textFont(createFont("Arial", 14));

  minim = new Minim(this);

  player = minim.loadFile("frequency.aif", 1024);
  player.loop();

  fft = new FFT(player.bufferSize(), player.sampleRate());

  println("sample rate: " + player.sampleRate());
  println("spec size: " + fft.specSize());
  println("bandwidth: " + fft.getBandWidth());
}

void draw() {
  background(0);

  // 現在再生中の音を周波数解析する
  fft.forward(player.mix);

  // それぞれの周波数帯域の強さを目標値にする
  target1 = fft.getBand(23) * 2;    // 約1000Hz
  target2 = fft.getBand(116) * 2;   // 約5000Hz
  target3 = fft.getBand(232) * 2;   // 約10000Hz

  // 目標値と現在の大きさの差を求める
  distance1 = target1 - obj1;
  distance2 = target2 - obj2;
  distance3 = target3 - obj3;

  // 差を少しずつ足して、現在の大きさを目標値に近づける
  obj1 = obj1 + distance1 / delay;
  obj2 = obj2 + distance2 / delay;
  obj3 = obj3 + distance3 / delay;

  // 滑らかに変化する円を描く
  fill(255);
  ellipse(100, 110, obj1, obj1);
  ellipse(200, 110, obj2, obj2);
  ellipse(300, 110, obj3, obj3);

  // ラベルを描く
  fill(255, 0, 0);
  textAlign(CENTER, CENTER);
  text("1000Hz", 100, 110);
  text("5000Hz", 200, 110);
  text("10000Hz", 300, 110);
}

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

配列を使って複数の周波数に反応させる

最後に、配列を使って複数の円を反応させます。ここでは233個の周波数帯域を使い、それぞれの強さに合わせて円の大きさを変えます。

リスト8
import ddf.minim.*;
import ddf.minim.analysis.*;

Minim minim;
AudioPlayer player;
FFT fft;

// 233個の円を扱うための配列
float[] obj = new float[233];      // 現在の円の大きさ
float[] target = new float[233];   // 目標の円の大きさ
float[] distance = new float[233]; // 現在値と目標値の差

float delay = 10.0;

void setup() {
  size(500, 220);
  noStroke();

  minim = new Minim(this);

  player = minim.loadFile("frequency.aif", 1024);
  player.loop();

  fft = new FFT(player.bufferSize(), player.sampleRate());
}

void draw() {
  background(0);

  // 現在再生中の音を周波数解析する
  fft.forward(player.mix);

  // 233個の周波数帯域を順番に調べる
  for (int i = 0; i < 233; i++) {

    // i番目の周波数帯域の強さを、円の目標サイズにする
    target[i] = fft.getBand(i) * 2;

    // 目標サイズと現在サイズの差を求める
    distance[i] = target[i] - obj[i];

    // 現在サイズを目標サイズに少しずつ近づける
    obj[i] = obj[i] + distance[i] / delay;

    // iの値を画面上のx座標に変換する
    float x = map(i, 0, 232, 0, width);

    // 元のサンプルと同じように、円ごとにランダムな色をつける
    fill(random(255), random(255), random(255), 180);

    // 周波数の強さに応じて円を描く
    ellipse(x, height / 2, obj[i], obj[i]);
  }
}

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

このように、FFTを使うと音の中の周波数成分を取り出し、グラフィックに反映できます。音量全体に反応させるだけでなく、低音、中音、高音など、特定の周波数帯域ごとに表現を変えることができます。