Fast Fourier Transform

Fast Fourier Transform

부품

  • Arduino Uno
  • MAX 4466 Mic

Fourier Transform

위 영상과는 형태가 다르겠지만, 어쿠스틱 공연용으로 기타 코드에 맞춰 반응하는 LED Wearable Suit 를 만들고 있던 중 소리를 분석하기 위한 알고리즘 Fourier Transform 을 테스트해 보았다.

단, 글쓴이가 이해된 부분만을 서술한 내용으로, 실제와는 다소 다를 수 있음

푸리에 변환(Fourier transform)은 한 함수를 인자로 받아 다른 함수로 변환하는 선형 변환이다. 일반적으로 변환된 함수는 원래 함수를 주파수 영역으로 표현한 것이라고 부른다.

푸리에 변환을 위키피디아에서 찾아보면 위와 같은 설명이 나온다.

Fourier Waves

사진을 보면서 설명하면 소리의 파동을 주파수로 변환하는 기능을 하는 것이 주로 푸리에 변환에서 많이 쓰인다고 한다. 솔직히 무슨소린지는 잘 모르겠지만, 파동을 선형 그래프로 변환해주는 역할을 하는 것이 푸리에 변환 이라고 한다. 이정도로 넘어가도록 하자.

Fast Fourier Transform

우리가 사용할 알고리즘은 사실 Fourier Transform 은 아니고 주로 FFT 라고 불리는 Fast Fourier Transform 이다. 한글명은 당연하게도 고속 푸리에 변환.

Sound Wave

우리가 생각하는 소리는 일반적으로 파동의 형태로 음원에서 귀로 전달된다. 소리는 진동이 짧을수록 높은소리, 진동이 길수록 높은 소리가 난다. 초당 진동이 몇번 생기는가를 우리는 주파수 라고 부르는데, 1초에 1번 생겼을때 1Hz 라고 표시한다. 따라서 소리를 우리는 Hz 로 구분할 수 있다. 절대음감을 컴퓨터 식으로 표현한다면 "기타의 맨 윗줄을 튕겼을 때 329.43 Hz 라고 판단이 가능하다" 라고 이야기할 수 있다.

Fast Fourier Transform

결국 소리를 분석하여 몇 번 주파수의 소리가 얼마만큼의 세기로 들어오는지를 분석하는 것이 Fast Fourier Transform 이라는 것이다. Wikipedia를 참고하라고 이야기하고 싶지만 전공자가 아닌 이상 영어 이상의 벽을 느낄것임을 말해두고 싶다.

Mic, Arudino & Processing

Electret Microphone Amplifier - MAX4466 with Adjustable Gain

위 사진은 Adafruit 에서 제작한 Electret Microphone Amplifier 즉 마이크로 FFT 알고리즘 테스트에 사용해보았다.

Code

FFT 라이브러리는 ELM Chan's FFT Library for Arduino 를 사용했다.

기본적으로 Arduino 에서 FFT Library 를 사용하여 소리를 분석하고, 이를 Serial Port 로 컴퓨터에 보내 Processing 에서 Visualize 하는 것으로 Oscilloscope 가 없는 환경에서 유용하게 사용할 수 있다. Mic의 OUT 을 A0로 연결하고 다음과 같이 코드를 작성한다.

링크 에서 다운받을 수 있다.

1. Arduino

#include <stdint.h>
#include <ffft.h>


#define  IR_AUDIO  0 // ADC channel to capture


volatile  byte  position = 0;
volatile  long  zero = 0;

int16_t capture[FFT_N];            /* Wave captureing buffer */
complex_t bfly_buff[FFT_N];        /* FFT buffer */
uint16_t spektrum[FFT_N/2];        /* Spectrum output buffer */

void setup()
{
  Serial.begin(57600);
  adcInit();
  adcCalb();
  establishContact();  // send a byte to establish contact until Processing respon
}

void loop()
{
  if (position == FFT_N)
  {
    fft_input(capture, bfly_buff);
    fft_execute(bfly_buff);
    fft_output(bfly_buff, spektrum);

    for (byte i = 0; i < 64; i++){
      Serial.write(spektrum[i]);
    }
   position = 0;
  }
}

void establishContact() {
 while (Serial.available() <= 0) {
      Serial.write('A');   // send a capital A
      delay(300);
  }
}

// free running ADC fills capture buffer
ISR(ADC_vect)
{
  if (position >= FFT_N)
    return;

  capture[position] = ADC + zero;
  if (capture[position] == -1 || capture[position] == 1)
    capture[position] = 0;

  position++;
}
void adcInit(){
  /*  REFS0 : VCC use as a ref, IR_AUDIO : channel selection, ADEN : ADC Enable, ADSC : ADC Start, ADATE : ADC Auto Trigger Enable, ADIE : ADC Interrupt Enable,  ADPS : ADC Prescaler  */
  // free running ADC mode, f = ( 16MHz / prescaler ) / 13 cycles per conversion 
  ADMUX = _BV(REFS0) | IR_AUDIO; // | _BV(ADLAR); 
//  ADCSRA = _BV(ADSC) | _BV(ADEN) | _BV(ADATE) | _BV(ADIE) | _BV(ADPS2) | _BV(ADPS1) //prescaler 64 : 19231 Hz - 300Hz per 64 divisions
  ADCSRA = _BV(ADSC) | _BV(ADEN) | _BV(ADATE) | _BV(ADIE) | _BV(ADPS2) | _BV(ADPS1) | _BV(ADPS0); // prescaler 128 : 9615 Hz - 150 Hz per 64 divisions, better for most music
  sei();
}
void adcCalb(){
  Serial.println("Start to calc zero");
  long midl = 0;
  // get 2 meashurment at 2 sec
  // on ADC input must be NO SIGNAL!!!
  for (byte i = 0; i < 2; i++)
  {
    position = 0;
    delay(100);
    midl += capture[0];
    delay(900);
  }
  zero = -midl/2;
  Serial.println("Done.");
}

Arduino SDK 에서 몇번째 포트에 연결되어 있는지 꼭 확인하고, 업로드 하자.

2. Processing

  myPort = new Serial(this, Serial.list()[7], 57600); 

Arduino SDK 에서 8번째 포트가 선택되었다면 아래 코드에서 위 부분을 찾아 list() 뒤의 [] 사이에 7을 입력하자. 컴퓨터는 일반적으로 0부터 셈하므로 7이 맞는 값이다. (3으로 해도 정상 동작한다. cu 와 tty 의 차이는 따로 기술하기로 한다.)

// Feel Free to edit these variables ///////////////////////////
String  xLabel = "Frequency";
String  yLabel = "Values";
String  Heading = "Arduino FFT";
String  URL = "01/02/2010";
float Vcc = 255.0;    // the measured voltage of your usb 
int NumOfVertDivisions=5;      // dark gray
int NumOfVertSubDivisions=10;  // light gray


int NumOfBars=64;    // you can choose the number of bars, but it can cause issues  
                    // since you should change what the arduino sends


// if these are changed, backgroung image has problems 
// a plain background solves the problem
int ScreenWidth = 800, ScreenHeight=600;
/////////////////////////////////////////////////////////

//  Serial port stuff ///////////////////////
import processing.serial.*;
Serial myPort;        
boolean firstContact = false; 
int[] serialInArray = new int[NumOfBars];
int serialCount = 0;
///////////////////////////////////////////////

int LeftMargin=100;
int RightMArgin=80;
int TextGap=50;
int GraphYposition=80; 
float BarPercent = 0.4;

int value;

PFont font;
PImage bg;

int temp;
float yRatio = 0.58;
int BarGap, BarWidth, DivisounsWidth;
int[] bars = new int[NumOfBars];

void setup(){
  /// NB SETTINGS ////////////////////////////////////////////////////////
  myPort = new Serial(this, Serial.list()[7], 57600); 
  ////////////////////////////////////////////////////////////////////////

  DivisounsWidth = (ScreenWidth-LeftMargin-RightMArgin)/(NumOfBars); 
  BarWidth = int(BarPercent*float(DivisounsWidth));
  BarGap = DivisounsWidth - BarWidth;

  size(ScreenWidth,ScreenHeight);
  font = createFont("Helvetica",12);

  textAlign(CENTER);
  textFont(font);
}

void draw(){
  if ( myPort.available() > 0 ) {
    println("Available!");
  }
  println(myPort.available());

  background(250);      // commented it out and put a plain colour 

  //  Headings();           // Displays bar width, Bar gap or any variable. 
  Axis();
  Labels();
  PrintBars();
//  Line();
//  Dots(); 
}




// Send Recieve data //
void serialEvent(Serial myPort) {
  println("Serial!");

  // read a byte from the serial port:
  int inByte = myPort.read();

  if (firstContact == false) {
    if (inByte == 'A') { 
      myPort.clear();          // clear the serial port buffer
      firstContact = true;     // you've had first contact from the microcontroller
      myPort.write('A');       // ask for more
    } 
  } 
  else {
    // Add the latest byte from the serial port to array:
    serialInArray[serialCount] = inByte;
    serialCount++;

    // If we have 6 bytes:
    if (serialCount > NumOfBars -1 ) {

for (int x=0;x<NumOfBars;x++){

  bars[x] = int (yRatio*(ScreenHeight)*(serialInArray[x]/256.0));

}


      // Send a capital A to request new sensor readings:
      myPort.write('A');
      // Reset serialCount:
      serialCount = 0;
    }
  }
}

/////// Display any variables for testing here//////////////
void Headings(){
  fill(0 );
  text("BarWidth",50,TextGap );   
  text("BarGap",250,TextGap );  
  text("DivisounsWidth",450,TextGap );
  text(BarWidth,100,TextGap );    
  text(BarGap,300,TextGap );    
  text(DivisounsWidth,520,TextGap );
}


void PrintBars(){ 

  int c=0;
  for (int i=0;i<NumOfBars;i++){

    fill((0xe4+c),(255-bars[i]+c),(0x1a+c));
    stroke(90);
    rect(i*DivisounsWidth+LeftMargin,   ScreenHeight-GraphYposition,   BarWidth,   -bars[i]);
    fill(0x2e,0x2a,0x2a);

    if ( i % 2 != 0 ) {
      text(i, i*DivisounsWidth+LeftMargin, ScreenHeight-GraphYposition+15);
    } else {
      text(i, i*DivisounsWidth+LeftMargin, ScreenHeight-GraphYposition+25);
    }

//    text(float(bars[i])/(yRatio*(ScreenHeight))*Vcc,   i*DivisounsWidth+LeftMargin+BarWidth/2,   ScreenHeight-bars[i]-5-GraphYposition );
//    text("A",   i*DivisounsWidth+LeftMargin+BarWidth/2 -5,   ScreenHeight-GraphYposition+20 );
//    text(i,   i*DivisounsWidth+LeftMargin+BarWidth/2 +5,   ScreenHeight-GraphYposition+20 );
  }
}

void Axis(){

  strokeWeight(1);
  stroke(220);
  for(float x=0;x<=NumOfVertSubDivisions;x++){

    int bars=(ScreenHeight-GraphYposition)-int(yRatio*(ScreenHeight)*(x/NumOfVertSubDivisions));
    line(LeftMargin-15,bars,ScreenWidth-RightMArgin-DivisounsWidth+50,bars);
  }
  strokeWeight(1);
  stroke(180);
  for(float x=0;x<=NumOfVertDivisions;x++){

    int bars=(ScreenHeight-GraphYposition)-int(yRatio*(ScreenHeight)*(x/NumOfVertDivisions));
    line(LeftMargin-15,bars,ScreenWidth-RightMArgin-DivisounsWidth+50,bars);
  }
  strokeWeight(2);
  stroke(90);
  line(LeftMargin-15, ScreenHeight-GraphYposition+2, ScreenWidth-RightMArgin-DivisounsWidth+50, ScreenHeight-GraphYposition+2);
  line(LeftMargin-15,ScreenHeight-GraphYposition+2,LeftMargin-15,GraphYposition+80);
  strokeWeight(1);
}

void Labels(){
  textFont(font,18);
  fill(50);
  rotate(radians(-90));
  text(yLabel,-ScreenHeight/2,LeftMargin-45);
  textFont(font,10);
  for(float x=0;x<=NumOfVertDivisions;x++){

    int bars=(ScreenHeight-GraphYposition)-int(yRatio*(ScreenHeight)*(x/NumOfVertDivisions));
    text(round(x),-bars,LeftMargin-20);
  }

  textFont(font,18);
  rotate(radians(90));  
  text(xLabel,LeftMargin+(ScreenWidth-LeftMargin-RightMArgin-50)/2,ScreenHeight-GraphYposition+40);
  textFont(font,24);
  fill(50);
  text(Heading,LeftMargin+(ScreenWidth-LeftMargin-RightMArgin-50)/2,70);
  textFont(font);

  fill(150);
  text(URL,ScreenWidth-RightMArgin-40,ScreenHeight-15);
  textFont(font);

}

Processing 코드를 실행하면 아래와 같은 창이 나오고, 연결이 정상적으로 되었으면, 막대 그래프가 위아래로 떨리는 모습을 확인할 수 있다.

FFT Visualizor for Processing

소리를 지르면서 테스트해보자. 아래와 같이 변화하면 잘 연결된 것이다.

여기까지의 참고 링크

3. Analysis

자 그럼 각 막대가 어떤 주파수를 의미하는지 알아보자. Elm-Chan Library 의 샘플링 레이트는 9.6 kHz 다. 라이브러리에서는 128 샘플(혹은 해상도)로 사용하지만 실제로는 앞의 64개밖에 사용할 수 없으므로 FFT Visualizor 에서는 64 개의 (Axis)막대기 그래프만 나타나고 있다. 0번째부터 셈하면 마지막 63번째 까지 막대라고 볼 수 있다.

한 막대의 크기를 계산하는 방법은 다음과 같다.

Sampling Rates / Samples(Resolution) = Frequency

즉 9,600 / 128 = 75 Hz 라고 계산되며 한 막대는 75 Hz 를 뜻하게 된다. 각 막대의 번호에 75 Hz 를 곱하면, 이제 우리는 0 Hz 부터 4,725 Hz 까지 측정할 수 있는 주파수 분석기를 갖게 되었다.

Tone Generator by Michael Heinz

역시 우리에겐 Tone 혹은 Frequency Generator 가 없으므로 앱을 받아서 이용해보자. 아이폰 유저의 경우 링크에서 다운 받을 수 있다. 안드로이드 폰에도 있을것이므로 위의 명칭으로 검색해보자.

1000 Hz 로 설정하고 테스트하면 위 사진과 같은 결과가 나온다. 13번째와 14번째 는 각각 975Hz 와 1050 Hz 이므로 정상적인 결과라고 할 수 있다. 그래프의 x 축은 주파수를, y 축은 소리의 세기를 표시하고 있다. 이 사이트에 올라와있는 프로세싱 코드는 몇번째 막대인지 알기 쉽게 일부 수정해두었다.

또한 아래 처럼 좀 더 작은 부품도 있으니 참고삼아 링크를 남겨둔다.

TS69868B 사운드 센서 보드 - MEMS형(Breakout Board for ADMP401 MEMS Microphone)

comments powered by Disqus