[Android] [FFT] 스마트폰으로 악기 음계 인식하기

sangsu.Park·2021년 9월 13일
1

https://github.com/Const4nt0228/Android-FFT-Sound-Recognizer

🐻 FFT ? DFT?

DFT (Discrete Fourier Transform)는 이산화된 시간 영역의 데이터를 이산화된 주파수 영역으로 변환해주는 알고리즘이며 FFT (Fast Fourier Transformaform)은 이를 빠르게 수행하는 알고리즘이다. 컴퓨터공학을 전공한다면 영상처리나 신호처리때 만나볼 수 있다.

이 글은 FFT를 설명하는 글이 아니라 이를 활용한 애플리케이션 구현이 목표이므로 스킵하도록하자..


🐻 FFT 라이브러리

자바에서 구현해놓은 FFT 라이브러리는 여러가지가 있지만 내가 구현한 앱에서는 JTransform 과 ca.uol.fftpack.RealDoubleFFT를 사용했다.

둘다 테스트 해보니 RealDoubleFFT가 잡음에 더 강한것 같아 지금은 이를 사용중이다.

🔻라이브러리 jar 파일
https://jar-download.com/artifacts/me.franfernandez/fftpack/0.1/source-code/ca/uol/aig/fftpack/RealDoubleFFT.java

libs 폴더안에 jar 파일을 넣고
app 단위 gradle에 추가 해줘야한다.

implementation files('libs\\fftpack-0.1.jar')
import ca.uol.aig.fftpack.RealDoubleFFT;

🐻 스마트폰 마이크 사용하기

사용전 먼저 매니페스트에 추가해주자

Manifest

    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

우리가 주로 사용하는 음향기기는 소리를 44100hz의 주파수로 끊어서 가져온다.
이 말은 즉 0hz ~ 44100hz의 주파수 대역대를 감지할 수 있다는 것을 의미한다

피아노에서 사용되는 1~8 옥타브의 경우 32hz~7902hz 의 범위를 가지기 때문에 딱 그만큼의 주파수만 잘라서 사용하면 된다. 참고로 frequency는 2의 n승의 범위를 사용하기 때문에
1024, 2048, 8192 이런식으로 사용해야한다.

   public int frequency = 8192; //주파수가 8192일 경우 4096 까지 측정이 가능함
   public int channelConfiguration = AudioFormat.CHANNEL_IN_MONO;
   public int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;
   private RealDoubleFFT transformer;
   int blockSize = 4096; 
  1. freqeuncy : FFT는 wave형식의 소리를 실수와 허수부로 나누어서 분석하기 때문에
    분석하고자 하는 주파수 범위의 2배의 값을 사용해야한다.
    만약 타겟 옥타브 범위가 1~8 옥타브일 경우에는 8192 hz까지 분석을 해야하나,
    FFT를 사용할 경우에는 8192 x 2 의 값으로 frequency를 사용해야한다.
    앱에서는 7옥타브 (4096이 타겟이므로) 8192를 인자 값으로 주었다.

  2. channelConfiguration : 음향기기는 채널 수에 따라 사운드가 나오는 방향이 여러개다.
    2채널 이어폰으로 음악을 들어본다면 왼쪽과 오른쪽에서 다른 소리가 나는것을 알 수있다.
    이번 개발에서는 한 채널만 사용할 것이기 때문에 MONO 타입을 사용했다.
    2채널로 쓰고싶다면 분석할 배열도 2배가 되야할것이다.

  3. blockSize : 한 주기에 분석할 frequency를 몇개로 쪼개서 배열에 넣을지다.

    ex)

    1. freq : 8192 / blockSize : 4096 -> 1hz 단위로 분석가능
    2. freq : 8192 / blockSize : 2048 -> 2hz 단위로 분석가능

(퍼런색 막대기가 몇 hz에서 소리가 나고있는지 알려준다(

앞서 말했듯이 freq가 8192일경우 분석가능한 주파수 대역대는 4096hz까지이다.
블록사이즈는 FFT를 분석해서 집어넣을 배열의 크기로 freq가 가진 크기만큼 블록사이즈를 설저안다면 깔끔하게 1hz씩 분석할수 있다.

하지만 2048개를 사용할 경우 막대기 하나당(배열 한칸) 2hz가 들어가게 되므로 분해능이 떨어지게 된다. 그렇지만 배열 갯수가 줄어들게 되므로 성능은 올라간다.

1옥타브는 주파수 간격이 2밖에 안될정도로 좁아서 무조건 freq와 같은 사이즈의 blockSize를 설정해야하지만, 그냥 연주때 주로 쓰이는 3~6옥타브를 타겟으로 할거면 분해능이 4hz정도여도 충분하다.


🐻AysncTask를 이용 녹음 및 FFT 변환

간단하다. short형으로 녹음되기 때문에 double로 형변환 해준뒤 라이브러리를 이용해서 변환시키면 끝!

transformer.ft(toTransform);
publishProgress(toTransform);
 public class RecordAudio extends AsyncTask<Void, double[], Void> {
        /* Context cContext;
         public RecordAudio(Context context){ cContext = context;}*/
        @Override
        protected Void doInBackground(Void... params) {
            try {
                // AudioRecord를 설정하고 사용한다.
                int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding);
                AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC, frequency, channelConfiguration, audioEncoding, bufferSize);
                // short로 이뤄진 배열인 buffer는 원시 PCM 샘플을 AudioRecord 객체에서 받는다.
                // double로 이뤄진 배열인 toTransform은 같은 데이터를 담지만 double 타입인데, FFT 클래스에서는 double타입이 필요해서이다.
                short[] buffer = new short[blockSize];
                double[] toTransform = new double[blockSize];
                double[] mag = new double[blockSize/2];

                audioRecord.startRecording();

                while (started) {
                    int bufferReadResult = audioRecord.read(buffer, 0, blockSize);
                    //FFT는 Double 형 데이터를 사용하므로 short로 읽은 데이터를 형변환 시켜줘야함. short / short.MAX_VALUE = double
                    for (int i = 0; i < blockSize && i < bufferReadResult; i++) {
                        toTransform[i] = (double) buffer[i] / Short.MAX_VALUE; // 부호 있는 16비트
                    }
                    transformer.ft(toTransform);
                    publishProgress(toTransform);
                }
                audioRecord.stop();
            } catch (Throwable t) {
                Log.e("AudioRecord", "Recording Failed");
            }
            return null;
        }

혹시 몰라서 JTransform을 사용하는 코드도 가져왔다(둘다 포함된 코드다)
JTransform은 실수 허수부를 직접 나눠서 넣어줘야한다.
주석처리한 부분을 확인하면 될듯하다.

 //두개의 FFT 코드를 사용 잡음잡는것은 RealDoubleFFT가 훨씬 더 잘잡는다.
                    //RealDoubleFFT 부분
                    transformer.ft(toTransform);

                    //-> JTransform 부분
                    //Jtransform 은 입력에 실수부 허수부가 들어가야하므로 허수부 임의로 0으로 채워서 생성해줌
                    /*double y[] = new double[blockSize];
                    for (int i = 0; i < blockSize; i++) {
                        y[i] = 0;
                    }
                    //실수 허수를 넣으므로 연산에는 blockSize의 2배인 배열 필요
                    double[] summary = new double[2 * blockSize];
                    for (int k = 0; k < blockSize; k++) {
                        summary[2 * k] = toTransform[k]; //실수부
                        summary[2 * k + 1] = y[k]; //허수부 0으로 채워넣음.
                    }
                    fft.complexForward(summary);
                    for(int k=0;k<blockSize/2;k++){
                        mag[k] = Math.sqrt(Math.pow(summary[2*k],2)+Math.pow(summary[2*k+1],2));
                    }*/
                    //Jtrans 끝

                    // publishProgress를 호출하면 onProgressUpdate가 호출된다.

                    publishProgress(toTransform);
                    //publishProgress(mag); //1D 로 쓸거면 mag, realdouble이면 toTrans

변환된 toTransform 배열은 다음 두가지를 내포한다

  1. 배열 번호 : 분석된 주파수이다. blocksize와 freq를 동일시 했으면 0번 배열이 0hz, freq는 4096만큼 분석하는데 blocksize가 2048이면 0번 배열이 0~1hz의 값을 의미한다.

  2. 배열 값 : 배열 안에 들어가있는 값이 소리의 크기이다. 타겟 주파수에서 60정도 크기까지 올라간다.


🐻 사용후기

스마트폰 마이크가 아닌 FFT 라이브러리 자체에도 분해능이 썩 좋진 않다. 동일 코드를 가지고 eclipse에서 테스트 해봐도 마찬가지의 결과가 나온다. 스마트폰 마이크 문제가아니라 코드 FFT 분해능이 문제라는거...

C4(4옥타브 도) 를 연주시 262hz만 튀어야하는데 그 두배인 524대역대도 덩달아서 올라간다.
이는 C5(5옥 도)의 주파수 대역대이다. C4를 연주했으면 C4만 인식해야하는데 C5도 인식해버린다는것... 이는 나중에 라벨링 작업때 어느정도 해소할 수있다. 조건문을 많이 붙이면 된다.

(하지만 끔찍한 괴물이 탄생했다)

플레이 스토어에 기타 튜너 어플은 어떻게 그렇게 잘되어있는지....

0개의 댓글