エフェクト、周波数解析

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

前回は音の基本と、minimライブラリを使って実際に音を再生し波形を見てみました。
その中で、実際に世の中にあふれている音は純音が合成された複合音のものが多いという話もしました。
今回は、その複合音の中の周波数を直接操作する方法を解説します。
このことによって、例えば再生した音楽の周波数によってグラフィックが変化していくというようなリアルタイム処理が可能になります。

map()

まず先に、音の操作とは直接関係ないのですが、以下のコードの中でmap()という関数が出てくるので、まずはその関数を理解しておきましょう。

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

という書き方をします。
value = 変更する変数
low1 = 変更前の範囲の最小値
high1 = 変更前の範囲の最大値
low2 = 変更後の範囲の最小値
high2 = 変更後の範囲の最大値

となり、要はvalueの値を変更前の範囲(low1〜high1)から変更後の範囲(low2〜high2)にしてくれる便利な関数です。
実際に簡単な例題をやってみましょう。

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

void draw(){
  background(255);
  stroke(0);
  //基準のため、マウスが動く範囲に線を作る
  //線のx座標の範囲は100から200の間
  line(100, height/2, 300, height/2);
  
  //mouseXが0から横幅の間を動く時、xは100から300の範囲の値を取る
  float x = map(mouseX, 0, width, 100, 300);
  fill(255, 0, 0);
  ellipse(x, height/2, 10, 10);    //円を描く
}

どうでしょうか。理解してしまえばかなり便利ですね。マウスアクションに結構使えると思います。

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

さて、ココからが本題です。まず、周波数を直接コントロールしてみます。まず、エフェクトの効果として代表的な、特定の周波数を通過(もしくはカット)させるフィルターを作成してみましょう。
このフィルターには大きく分けて3種類あります。

1. ローパスフィルタ(低域通過フィルタ)
2. ハイパスフィルタ(高域通過フィルタ)
3. バンドパスフィルタ(帯域通過フィルタ)

今回はローパスフィルタとハイパスフィルタを作ってみます。バンドパスフィルタは、前述の2つのフィルタの組み合わせになるのでここでは触れません。
まずは、ローパスフィルタです。音声ファイルは以下からダウンロードしてください。

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.

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

Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
LowPassSP lpf;  //ローパスフィルター用の変数
float cutoff;    //削除する周波数のしきい値
int waveH;    //波形の高さ

void setup(){
  size(512, 200);
  minim = new Minim(this);  //初期化
  //sample.mp3をロードして、バッファを確保(1024k)
  player = minim.loadFile("sample.mp3", 1024);
  player.loop();  //ループ再生

  //5000Hzでローパスフィルターを設定する。
  //2つ目の引数は、サンプルレート。このサンプルレートは、正確な計算のため必要。
  lpf = new LowPassSP(5000, player.sampleRate());
  player.addEffect(lpf);    //エフェクトを設定
  
  waveH = 50;    //波形の高さを50に設定
}

void draw(){
  background(0);
  stroke(255);
  //波形を描く
  for ( int i = 0; i < player.bufferSize() - 1; i++ ){
    //左の音は上に、右の音は下に表示。
    //left.get()とright.get()は1から-1の間の値を取るので、見やすいようにwaveHを掛ける。
    point(i, 50 + player.left.get(i)*waveH);	//左の音声の波形を画面上に描く
    point(i, 150 + player.right.get(i)*waveH);	//右  〃
  }
}

//もしマウスが動いたら、
void mouseMoved(){

  //mouseXが0~512(横幅)の時に、そのmouseXの範囲を20~5000に変更する。
  cutoff = map(mouseX, 0, width, 20, 5000);
  lpf.setFreq(cutoff);    //ローパスフィルターを設定し直す
}

void mousePressed(){

  println(cutoff);    //しきい値を表示
}

void stop(){

  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();

  //ソフト全体を終了
  super.stop();
}

次にハイパスフィルタです。

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.

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

Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
HighPassSP hpf;  //ハイパスフィルター用の変数
float cutoff;    //削除する周波数のしきい値
int waveH;    //波形の高さ


void setup(){
  size(512, 200);
  minim = new Minim(this);  //初期化
  //sample.mp3をロードして、バッファを確保(1024k)
  player = minim.loadFile("sample.mp3", 1024);
  player.loop();  //ループ再生

  //1000Hzでハイパスフィルターを設定する。
  //2つ目の引数は、サンプルレート。このサンプルレートは、正確な計算のため必要。
  hpf = new HighPassSP(100, player.sampleRate());
  player.addEffect(hpf);    //エフェクトを設定

  waveH = 50;    //波形の高さを50に設定
}

void draw(){

  background(0);
  stroke(255);

  //波形を描く
  for(int i = 0; i < player.bufferSize() - 1; i++){

    //左の音は上に、右の音は下に表示。
    //left.get()とright.get()は1から-1の間の値を取るので、見やすいようにwaveHを掛ける。
    point(i, 50 + player.left.get(i)*waveH);	//左の音声の波形を画面上に描く
    point(i, 150 + player.right.get(i)*waveH);	//右  〃
  }
}

//もしマウスが動いたら、
void mouseMoved(){

  //mouseXが0~512(横幅)の時に、そのmouseXの範囲を100~10000に変更する。
  cutoff = map(mouseX, 0, width, 100, 10000);
  hpf.setFreq(cutoff);    //ハイパスフィルターを設定し直す
}

void mousePressed(){

  println(cutoff);    //しきい値を表示
}

void stop(){

  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();

  //ソフト全体を終了
  super.stop();
}

基本的にエフェクトは、このように特定の周波数を増減したりして変化させます。
しかし、minimには汎用的なエコー、リバーブなどのエフェクトは標準で付いていません。それだけ原理的な所から音声を操作することができると言えます。

FFT (Fast Fourier Transform – 高速フーリエ変換)

周波数を解析するのに、よく使われるアルゴリズムです。殆どのサウンドライブラリはFFTの機能があり、複雑な数式を書いて周波数解析をする必要がありません(僕も周波数解析のアルゴリズムを説明しろと言われてもできない)。
どうしても原理を知りたい人はWikipediaを見てください。
FFTの説明

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

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

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
FFT fft;    //フーリエ変換用変数
void setup(){
  size(1024, 200);
  stroke(0);
  minim = new Minim(this);
  //バッファはfloat型の配列のサイズだと考えると理解しやすい。
  //このサンプルのバッファサイズは1024
  player = minim.loadFile("frequency.aif", 1024);
  player.loop();
  //FFTオブジェクトを作成。bufferSize()は1024、sampleRateは初期設定では44100Hz。
  fft = new FFT(player.bufferSize(), player.sampleRate());
  println("sample rate is " +player.sampleRate());
  println("spec size is " +fft.specSize());
  println("bandwidth is: " +fft.getBandWidth());    //最小単位の帯域
}
void draw(){
  background(255);
  //fftを左と右の音声を混ぜて解析
  //左右の音をそれぞれ取りたければ、player.left、player.rightという形で使う
  fft.forward(player.mix);
  //サンプリングレートが44100Hzの場合、実際の周波数はその半分の0~22050Hzしか入っていない。
  //なので、バッファサイズが1024だとすると、specSize()はバッファ/2 +1 になる。つまり513。
  //また、バッファが1024ということは、44100Hzを1024分割していることになり、結果BandWidthは43.066406になる。
  for(int i = 0; i < fft.specSize() - 1; i++){
    //画面の下から上に延びる線を描く
    line(i, height, i, height - fft.getBand(i));
  }
}
void stop(){
  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();
  //ソフト全体を終了
  super.stop();
}

サンプルのサウンドを再生させてみてください。
この周波数帯域は全体で44100Hz(44.1kHz)なので、かなり高音に聞こえる音でも10000Hzぐらいだということが分かったでしょうか?これで、周波数を解析することができました。この抽出した周波数に対して、何かグラフィックを当てはめてみましょう。

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

しかし、この周波数帯の理解はなかなか難しいのが実状です。ここで以下のサンプルを実行させてみます。
このサンプルは、1000Hz, 5000Hz, 10000Hzの音がどの帯域に含まれているかを理解し、的確に形に反映させるためのものです。

特に、void setup()の中の以下のコードが重要になります。

for(int i = 0; i < fft.specSize(); i++){ 
 
  println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
}

サンプリングレート→44100Hz
バッファサイズ→1024
スペックサイズ→バッファサイズ/2 + 1 = 513
だとすると、
44100/1024で、一つ一つの周波数帯域(ブロック)は43.066406Hzとなります。
つまりこの幅の周波数帯域が1024個できます。しかし、20000Hz以上は音が入っていないので、視覚化するときにはスペックサイズの方を使うといいでしょう。スペックサイズはこの場合、513になります。

そしてこのコードを実行することによって、特定の周波数(例えば1000Hz)が何番目のブロックにあるかが分かるのです。実際に実行してみると、以下の値が出力されます(ここでは26番目のブロックまで出力しています)。

すると、1000Hzは23番目のブロックに含まれていることが分かります。
よって、1000Hzの周波数の音に円の大きさを反応させたい場合には、draw関数内で以下のように記述します。

ellipse(100, 100, fft.getBand(23), fft.getBand(23));    //1000Hzはi = 23の周波数対に含まれる

以下が完全なコードです。

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
FFT fft;    //フーリエ変換用変数
PFont font;
void setup(){
  size(400, 200);
  noStroke();
  font = createFont("Arial", 14);
  textFont(font);
  minim = new Minim(this);
  //バッファはfloat型の配列のサイズだと考えると理解しやすい。
  //このサンプルのバッファサイズは1024
  player = minim.loadFile("sin01000.AIF", 1024);    //ここでは1000Hzの音を再生する
  player.loop();
  //FFTオブジェクトを作成。bufferSize()は1024、sampleRateは再生するサウンドのサンプリングレートによる。
  //通常、44100Hzか22050Hz。このサンプルは44100Hz。
  fft = new FFT(player.bufferSize(), player.sampleRate());
  println("sampling reate is " +player.sampleRate());
  println("spec size is " +fft.specSize());
  println("bandwidth is: " +fft.getBandWidth());
  //BandWidthにiを掛けると、それぞれ何番目のブロックに目当ての周波数が含まれるかが分かる。
  //コンソール(一番したのエリア)を確認すること
  for(int i = 0; i < fft.specSize(); i++){  
    println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
  }
}
void draw(){
  background(0);
  //stroke(255);
  //fftを左と右の音声を混ぜて解析
  //左右の音をそれぞれ取りたければ、player.left、player.rightという形で使う
  fft.forward(player.mix);
  fill(255);
  ellipse(100, 100, fft.getBand(23), fft.getBand(23));    //1000Hzはi = 23の周波数帯域に含まれる
  ellipse(200, 100, fft.getBand(116), fft.getBand(116));    //5000Hzはi = 116のの周波数帯域に含まれる
  ellipse(300, 100, fft.getBand(232), fft.getBand(232));     //10000Hzはi = 232のの周波数帯域に含まれる
  fill(255, 0, 0);
  text("1000Hz", 80, 100);    //テキスト表示
  text("5000Hz", 180, 100);
  text("10000Hz", 280, 100);
} 
void stop(){
  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();
  //ソフト全体を終了
  super.stop();
}

動きを滑らかにする

ゆるやかに形やサイズ、色を変化させたい場合には、以下のサンプルを参考にします。

float objY;    //オブジェクトのx, y座標
float disY;    //mouse座標とオブジェクトの距離
//mouseが押されたときに、一時的にx, y座標を保存しておくための変数
float targetY;
float delay = 10.0;    //マウスに遅れる度合い
void setup() {
  size(400, 400);
  background(255);
  noStroke();
  fill(0);
  
  objY = targetY = 0;
}
void draw() {
  background(255);
  if (mousePressed) targetY = mouseY;
  else targetY = 0;
  //マウス座標とオブジェクトの距離をdisYに入れる
  disY = targetY - objY;
  //距離(disX, disY)をdelayで割った値を足す(オブジェクトが移動する)
  objY = objY + disY/delay;
  rect(0, 0, width, objY);
}

このサンプルを基にして、FFTのコードを修正すると以下になります。

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
FFT fft;    //フーリエ変換用変数
PFont font;
float obj1, dis1, target1;
float obj2, dis2, target2;
float obj3, dis3, target3;
float delay = 10.0;
void setup()
{
  size(400, 200);
  noStroke();
  font = createFont("Arial", 14);
  textFont(font);
  minim = new Minim(this);
  //バッファはfloat型の配列のサイズだと考えると理解しやすい。
  //このサンプルのバッファサイズは1024
  player = minim.loadFile("frequency.aif", 1024);    //ここでは1000Hzの音を再生する
  player.loop();
  //FFTオブジェクトを作成。bufferSize()は1024、sampleRateは再生するサウンドのサンプリングレートによる。
  //通常、44100Hzか22050Hz。このサンプルは44100Hz。
  fft = new FFT(player.bufferSize(), player.sampleRate());
  println("sampling reate is " +player.sampleRate());
  println("spec size is " +fft.specSize());
  println("bandwidth is: " +fft.getBandWidth());
  //BandWidthにiを掛けると、それぞれ何番目のブロックに到底の周波数が含まれるかが分かる。
  //コンソール(一番したのエリア)を確認すること
  for (int i = 0; i < fft.specSize(); i++)
  {  
    println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
  }
}
void draw()
{
  background(0);
  //stroke(255);
  //fftを左と右の音声を混ぜて解析
  //左右の音をそれぞれ取りたければ、player.left、player.rightという形で使う
  fft.forward(player.mix);
  target1 = fft.getBand(23);  //到達地点のtarget1に23番目の周波数の値を入れる
  dis1 = target1 - obj1;      //target1からオブジェクトまでの距離を計算
  obj1 = obj1 + dis1 / delay;  //現在のオブジェクトの大きさを計算
  target2 = fft.getBand(116);
  dis2 = target2 - obj2;
  obj2 = obj2 + dis2 / delay;
  target3 = fft.getBand(232);
  dis3 = target3 - obj3;
  obj3 = obj3 + dis3 / delay;
  fill(255);
  ellipse(100, 100, obj1, obj1);    //1000Hzはi = 23の周波数対に含まれる
  ellipse(200, 100, obj2, obj2);    //5000Hzはi = 116のの周波数に含まれる
  ellipse(300, 100, obj3, obj3);     //10000Hzはi = 232のの周波数に含まれる
  fill(255, 0, 0);
  text("1000Hz", 80, 100);    //テキスト表示
  text("5000Hz", 180, 100);
  text("10000Hz", 280, 100);
} 
void stop()
{
  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();
  //ソフト全体を終了
  super.stop();
}

最後に配列を使って、複数(この場合は233個)の円を反応させましょう。

//written by  by Damien Di Fede.
//arranged by Yasushi Noguchi.
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim; //Minim型変数であるminimの宣言
AudioPlayer player;  //サウンドデータ格納用の変数
FFT fft;    //フーリエ変換用変数
PFont font;
float[] obj = new float[233];
float[] dis = new float[233];
float[] target = new float[233];
float delay = 10.0;
void setup()
{
  size(500, 200);
  noStroke();
  font = createFont("Arial", 14);
  textFont(font);
  minim = new Minim(this);
  //バッファはfloat型の配列のサイズだと考えると理解しやすい。
  //このサンプルのバッファサイズは1024
  player = minim.loadFile("frequency.aif", 1024);    //ここでは1000Hzの音を再生する
  player.loop();
  //FFTオブジェクトを作成。bufferSize()は1024、sampleRateは再生するサウンドのサンプリングレートによる。
  //通常、44100Hzか22050Hz。このサンプルは44100Hz。
  fft = new FFT(player.bufferSize(), player.sampleRate());
  println("sampling reate is " +player.sampleRate());
  println("spec size is " +fft.specSize());
  println("bandwidth is: " +fft.getBandWidth());
  //BandWidthにiを掛けると、それぞれ何番目のブロックに到底の周波数が含まれるかが分かる。
  //コンソール(一番したのエリア)を確認すること
  for (int i = 0; i < fft.specSize(); i++)
  {
    println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
  }
}
void draw()
{
  background(0);
  //fftを左と右の音声を混ぜて解析
  //左右の音をそれぞれ取りたければ、player.left、player.rightという形で使う
  fft.forward(player.mix);
  for (int i = 0; i < 233; i ++) {
    target[i] = fft.getBand(i) * 2;  //到達地点のtarget1に23番目の周波数の値を入れる
    dis[i] = target[i] - obj[i];      //target1からオブジェクトまでの距離を計算
    obj[i] = obj[i] + dis[i] / delay;  //現在のオブジェクトの大きさを計算
    fill(random(255), random(255), random(255));
    ellipse(i * 2, 100, obj[i], obj[i]);    //1000Hzはi = 23の周波数帯に含まれる
  }
} 
void stop()
{
  // アプリケーションの終了前にAudioPlayerを終了する
  player.close();
  // minimを終了
  minim.stop();
  //ソフト全体を終了
  super.stop();
}