CGプログラミング演習
9 音の視覚化の基本的な考え方

今回は、音を視覚化するための基礎的な力を養うことを目的とします。

1 音の視覚化

1.1 音の視覚化の基本的な考え方

  • 音は、電波や光などと同じ波形としての性質を持っています。
  • プログラムとしての素直なつくりは、音を波形そのままに扱うことですが、それでは、人間にとって分かりにくい。
  • そこで、音を周波数(音色の高低)ごとに分解する方法をとるのが、音を視覚化するにあたっての基本的な手法となります。
  • つまり、周波数ごとに分解した音色のそれぞれに応じたグラフィックを与えるのです。
  • 音のもうひとつの特徴は、それが時間軸をもっている点です。
  • これは画像とまったく異なる特徴と言えます。画像は、基本的に同時的に捉えられるからです。
  • 音の時間軸をどのように描画するかという問題は非常におくの深いテーマと言えます。

音の波形情報はサインカーブで表現できる。

1.2 Processing で音を扱うには

  • Processing で音を扱うには、Minim ライブラリを利用します。
  • Minim ライブラリはとても優秀なライブラリで、下記の機能を簡単に利用することが可能です。
    • 音に関する様々な機能が利用できる
    • サウンドの再生………WAV, AIFF, AU, SND, MP3形式のサウンドファイルを読み込んで再生
    • 録音………入力された音を、ディスクに直接、またはメモリー上のバッファーに録音可能
    • オーディオ入力………モノラル、ステレオのオーディオ入力
    • オーディオ出力………モノラル、ステレオでサウンドを出力
    • 音響合成………シンプルな音響合成のための機能
    • エフェクト………サウンドエフェクトのためのインターフェース
    • FFT………リアルタイムにスペクトル解析が可能

2 フーリエ解析の利用

2.1 フーリエ解析の概要

  • 波形としての音を、周波数ごとに分解する方法としては、フーリエ解析が一般的です。
  • この授業でも、Minim ライブラリの持つ、フーリエ解析機能「高速フーリエ変換(FFT: First Fourier Transform)」を利用します。
  • 音などの波形情報を分析し、その結果を視覚化したものを「スペクトログラム」といいます。
  • スペクトログラムは、声紋の鑑定や動物の鳴き声の分析などに用いられます。
    参考:スペクトログラム - Wikipedia

バイオリンのスペクトログラム。スペクトログラムとは、周波数を解析して、その内容を視覚化したもの。

2.2 Minim でのフーリエ変換(高速フーリエ変換 FFT: First Fourier Transform)の方法

  • FFT の使用準備(インスタンス生成)
FFT(int timeSize, float sampleRate)
  • FFTで波形を変換(周波数からスペクトラムへ)
forward(AudioBuffer buffer, int offset)
  • 指定した周波数帯の音量を取り出す
float getBand(int i)

3 サンプルプログラム

3.1 基本的なサンプルプログラム(波形の棒グラフ化)

  • 以下、AudioInput からの入力信号を FFT して表示してみます。
  • まずは単純な棒グラフでの表示例です。

// Minim を使用する準備です
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim;

// オーディオ入力の変数を用意します
AudioInput in;

// FFTの変数を用意します
FFT fft;

// プログラム開始時の事前準備です
void setup()
{
  // キャンバスサイズを指定します
  size(600, 300);

  // Minim を初期化します
  minim = new Minim(this);

  // ステレオオーディオ入力を取得します
  in = minim.getLineIn(Minim.STEREO, 512);

  // ステレオオーディオ入力を FFT と関連づけます
  fft = new FFT(in.bufferSize(), in.sampleRate());
}

// 描画内容を定めます
void draw()
{
  // 背景色を黒に設定
  background(0);
  
  // 線の色を白に設定
  stroke(255);

  // FFT 実行
  fft.forward(in.mix);

  // FFTのスペクトラムの幅を変数に保管します
  int specSize = fft.specSize();

  // 棒グラフを描画します
  for (int i = 0; i < specSize; i++)
  {
    // x をスペクトラム幅に応じた位置として取得します
    float x = map(i, 0, specSize, 0, width);
    
    // fft.getBand(i) で、個別のスペクトラムの値を取得し、
    // 取得した値に応じた線を描きます。
    line(x, height, x, height - fft.getBand(i) * 8);
  }
}

// プログラム終了時の処理を定めます
void stop()
{
  minim.stop();
  super.stop();
}
  • これまでの画像解析プログラムと基本的な構造は同じです。
  • が、見慣れない関数が含まれていますし、とくに for 文のところは、かなり趣が違っています。
  • そこで、以下、重要な箇所をピックアップして説明します。

3.2 プログラム解説

  • 以下、上記プログラムから、draw() { ... } の中の重要箇所を順を追ってみていきます。

3.2.1 FFTの実行

  • まず fft.forward(in.mix); とある部分。
    ここで、入力源(in.mix)を指定して、FFT しています。
  • ここでは、左右の入力音を mix して FFT していますが、これを in.right とすると、右側の入力源のみを対象として FFT できます。
  // FFT 実行
  fft.forward(in.mix);

3.2.2 棒グラフの「左右軸=スペクトル幅」「上下軸=音量」

  • つづく箇所で、棒グラフを描画しています。
  • この棒グラフは、上下、左右方向に下記のような軸線を用いて描画しています。

  • スペクトル幅
    • 棒グラフの左右方向は、周波数をあらわします。
    • スペクトル幅とは、この分解された音の周波数幅のことです。
    • スペクトル幅は、fft.specSize() で取得します。
  // FFTのスペクトラムの幅を変数に保管します
  int specSize = fft.specSize();
  • 音量
    • 棒グラフの上下方向が、音量を表わしています。
    • 音量は、fft.getBand(i) で取得します。

3.2.3 棒グラフの描画

  • 棒グラフ
    • 棒グラフの具体的な描画は、for 文の中で行っています。
    • ここで、スペクトラムを左から順においかけつつ(ispecSize)、それぞれの周波数の音を取り出して、棒グラフにしています。
  // 棒グラフを描画します
  for (int i = 0; i < specSize; i++)
  {
    // x をスペクトラム幅に応じた位置として取得します
    float x = map(i, 0, specSize, 0, width);
    
    // fft.getBand(i) で、個別のスペクトラムの値を取得し、
    // 取得した値に応じた線を描きます。
    line(x, height, x, height - fft.getBand(i) * 8);
  }
  • 上記のうち、float x = map(i, 0, specSize, 0, width); とある部分、ここではじめて見る関数 mapが使用されています。
  • map 関数は、下記のような機能を持つ関数です。
// map 関数は、ある範囲[low1 〜 high1]における特定の値[value]が、別の範囲では[low2 〜 high2]どこに位置するかを計算します。
// たとえば、下記の場合……

float value = 3;                // 値として 3
float low1 = 2, high1 = 6;      // 最初の範囲として 2〜6
float low2 = 4, high2 = 12;     // 次なる範囲として 4〜12

// 上の条件で map 関数を使用し、結果を answer 変数に保存
float answer = map(value, low1, high1, low2, high2);

// answer の内容を確認してみると、answer には、6.0 が代入されます。
print(answer);
  • この説明だけだと、少し分かりにくいと思いますので、図にしてみます。

  • [low1〜high1]を[low2〜high2]に拡大/縮小するイメージで捉えると分かりやすいと思います。
  • 結局、for 文の中にあった下記部分は……
    // x をスペクトラム幅に応じた位置として取得します
    float x = map(i, 0, specSize, 0, width);
  • スペクトラム幅[0 〜 specSize]を、ウインドウサイズ[0 〜 width]に拡大/縮小しているわけです。

3.3 棒グラフに色をつけてみる

  • 次のサンプルプログラムは、さきほどの棒グラフの周波数ごとに色をつけたものです。

// Minim を使用する準備です
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim;

// オーディオ入力の変数を用意します
AudioInput in;

// FFTの変数を用意します
FFT fft;

// プログラム開始時の事前準備です
void setup()
{
  // キャンバスサイズを指定します
  size(600, 300);

  // Minim を初期化します
  minim = new Minim(this);

  // ステレオオーディオ入力を取得します
  in = minim.getLineIn(Minim.STEREO, 512);

  // ステレオオーディオ入力を FFT と関連づけます
  fft = new FFT(in.bufferSize(), in.sampleRate());
  
  colorMode(HSB, 360, 100, 100, 100);
}

// 描画内容を定めます
void draw()
{
  // 背景色を黒に設定
  background(0);
  
  // 線の色を白に設定
  stroke(0, 255, 255);

  // FFT 実行
  fft.forward(in.mix);

  // FFTのスペクトラムの幅を変数に保管します
  int specSize = fft.specSize();

  // 棒グラフを描画します
  for (int i = 0; i < specSize; i++)
  {
    // 線を描く位置に応じて、色相を変化させます。
    float h = map(i, 0, specSize, 0, width);
    
    // 線の色を設定します。
    stroke(h, 100, 100, 100);
    
    // 線を描く x を、スペクトラム幅に応じた位置として取得します
    float x = map(i, 0, specSize, 0, width);
    
    // fft.getBnad 
    line(x, height, x, height - fft.getBand(i) * 12);
  }
}

// プログラム終了時の処理を定めます
void stop()
{
  minim.stop();
  super.stop();
}

3.4 透明度で音を表現する

  • 上の 2 つのサンプルプログラムは棒グラフで音量を表現してきましたが、透明度によって音量を表現してみます。
  • ここまでは、画像を解析した時とほとんど同じです。

// Minim を使用する準備です
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim;

// オーディオ入力の変数を用意します
AudioInput in;

// FFTの変数を用意します
FFT fft;

// FFT が利用するメモリサイズを定めます
int BUFSIZE = 512;

// プログラム開始時の事前準備です
void setup()
{
  // キャンバスサイズを指定します
  size(600, 300);

  // Minim を初期化します
  minim = new Minim(this);

  // ステレオオーディオ入力を取得します
  in = minim.getLineIn(Minim.STEREO, BUFSIZE);

  // ステレオオーディオ入力を FFT と関連づけます
  fft = new FFT(in.bufferSize(), in.sampleRate());
  
  // カラーモードを HSB に指定します
  colorMode(HSB, 360, 100, 100, 100);
  
  // 線を描画しません
  noStroke();
}

// 描画内容を定めます
void draw()
{
  // 背景色を黒に設定
  background(0);

  // FFT 実行(左チャンネル)
  fft.forward(in.left);
  
  // FFTのスペクトラムの幅を変数に保管します
  float specSize = fft.specSize();

  // 棒グラフを描画します
  for (int i = 0; i < specSize; i++)
  {
    // 棒の左右位置に応じた色相を取得します
    float h = map(i, 0, specSize, 0, 360);
    
    // スペクトラムの値に応じた不透明度を取得します
    float a = map(fft.getBand(i), 0, BUFSIZE/16, 0, 255);
    
    // スペクトラム位置に応じた棒の x 位置を取得します。
    float x = map(i, 0, specSize, width/2, 0);
    
    // 棒の描画幅を定めます
    float w = width / specSize / 2;
    
    // 棒の塗りつぶし色を指定します
    fill(h, 80, 80, a);
    
    // 棒を描画します
    rect(x, 0, w, height);
  }

  // FFT 実行(右チャンネル)
  fft.forward(in.right);
  
  // FFTのスペクトラムの幅を変数に保管します
  specSize = fft.specSize();

  // 棒グラフを描画します
  for (int i = 0; i < specSize; i++)
  {
    // 棒の左右位置に応じた色相を取得します
    float h = map(i, 0, specSize, 0, 360);
    
    // スペクトラムの値に応じた不透明度を取得します
    float a = map(fft.getBand(i), 0, BUFSIZE/16, 0, 255);
    
    // スペクトラム位置に応じた棒の x 位置を取得します。
    float x = map(i, 0, specSize, width/2, width);
    
    // 棒の描画幅を定めます
    float w = width / specSize / 2;
    
    // 棒の塗りつぶし色を指定します
    fill(h, 80, 80, a);
    
    // 棒を描画します
    rect(x, 0, w, height);
  }
}

// プログラム終了時の処理を定めます
void stop()
{
  minim.stop();
  super.stop();
}

3.5 円の色と大きさで音を表現する

  • 今日最後のサンプルプログラムとして、円の色と大きさで音を表現したものを示します。

// Minim を使用する準備です
import ddf.minim.analysis.*;
import ddf.minim.*;
Minim minim;

// オーディオ入力の変数を用意します
AudioInput in;

// FFTの変数を用意します
FFT fft;

// FFT が利用するメモリサイズを定めます
int BUFSIZE = 512;

// プログラム開始時の事前準備です
void setup()
{
  // キャンバスサイズを指定します
  size(600, 300);

  // Minim を初期化します
  minim = new Minim(this);

  // ステレオオーディオ入力を取得します
  in = minim.getLineIn(Minim.STEREO, BUFSIZE);

  // ステレオオーディオ入力を FFT と関連づけます
  fft = new FFT(in.bufferSize(), in.sampleRate());
  
  // カラーモードを HSB に指定します
  colorMode(HSB, 360, 100, 100, 100);
  
  // 線を描画しません
  noStroke();
}

// 描画内容を定めます
void draw()
{
  // 背景色を 10% の黒で塗りつぶします
  // 前フレームの内容が薄く残ります
  fill(0, 0, 0, 10);
  rect(0, 0, width, height);

  // FFT 実行(左チャンネル)
  fft.forward(in.left);
  
  // FFTのスペクトラムの幅を変数に保管します
  float specSize = fft.specSize();

  // スペクトラムに応じて円を描画します
  for (int i = 0; i < specSize; i++)
  {
    float h = map(i, 0, specSize, 0, 180);
    float ellipseSize = map(fft.getBand(i), 0, BUFSIZE/16, 0, width/2);
    float x = map(i, 0, fft.specSize(), width/2, 0);
    fill(h, 80, 80, 7);
    ellipse(x, height/2, ellipseSize, ellipseSize);
  }

  // FFT 実行(右チャンネル)
  fft.forward(in.right);

  // FFTのスペクトラムの幅を変数に保管します
  specSize = fft.specSize();

  // スペクトラムに応じて円を描画します
  for (int i = 0; i < specSize; i++)
  {
    float h = map(i, 0, specSize, 0, 180);
    float ellipseSize = map(fft.getBand(i), 0, BUFSIZE/16, 0, width/2);
    float x = map(i, 0, specSize, width/2, width);
    fill(h, 80, 80, 7);
    ellipse(x, height/2, ellipseSize, ellipseSize);
  }
}

// プログラム終了時の処理を定めます
void stop()
{
  minim.stop();
  super.stop();
}

4 演習(時間があれば)

  • 上の 3つのサンプルプログラムをアレンジしてください。
  • 音の視覚化に少しでも慣れることが目的です。