音響解析[Processing]フーリエ変換->Sin波


開発環境


言語

- Processing 3.1.1

eclipseを使用し、core.jarをインポートした環境でコーディングしているため、ProcessingのIDEでは実行できません。

参考URL

http://hiroyukitsuda.com/archives/1721

- java 1.8.0_91

API

OS

- macOS sierra 10.12.2

ソフトウェア

次のソフトウェアでitunesなどの音源をオーディオ入力として扱うことができます

- LadioCast

https://itunes.apple.com/jp/app/ladiocast/id411213048?mt=12

- Soundflower

https://soundflower.en.softonic.com/mac

参考サイト、書籍

http://yoppa.org/cuc_proga11/2961.html

フーリエ変換の説明もあり、わかりやすかったです。

http://r-dimension.xsrv.jp/classes_j/frequency/

[特定の周波数に反応させる]の章で、Minimの具体的な利用方法を学べました。


概要

音声をフーリエ変換し、取得した周波数をサイン波で表示します。


フロー

1.オーディオ入力された音声をキャプチャ

2.フーリエ変換のFFTオブジェクトを生成

3.オーディオ入力された音声をフーリエ変換し、周波数情報をリストに格納

4.フーリエ変換の結果を棒グラフで表示

5.以下のカテゴリ毎に、最大音圧の周波数を取得

低域

中域

中高域

高域

全域

6.取得した最大音圧の周波数をSin波でそれぞれ表示


1.オーディオ入力された音声をキャプチャ

MinimのgetLineInメソッドでオーディオ入力された音声をキャプチャします。

後のフーリエ変換でより細かいブロックで周波数帯域を取得したいため、バッファサイズを4096に指定しています。

public void setup() {
                //Minimの初期化
                minim = new Minim(this);
                //ステレオ,バッファサイズ4096でオーディオ入力音を取得するように指定
                audioInput = minim.getLineIn(Minim.STEREO,4096);

2.フーリエ変換のFFTオブジェクトを作成

以下設定でFFTオブジェクトを生成します。

バッファサイズ   :44100Hz

サンプリングレート :4096

上記設定により、ひとつひとつの周波数帯域は44100/4096の10.7666015625Hzとなります。

        public void setup() {
                //~~~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
                //フーリエ変換のFFTオブジェクトを生成
                fft = new FFT(audioInput.bufferSize(), audioInput.sampleRate());
                //周波数帯域を出j力
                for(int i = 0; i < fft.specSize(); i++)
                {
                        println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
                }
                //窓関数にハミング窓を指定
                fft.window(FFT.HAMMING);

コンソールの出力内容から、

10.7666015625Hz単位で周波数帯域のブロックが作成されていることが確認できます。

0 = 0.0 ~ 10.766602
1 = 10.766602 ~ 21.533203
2 = 21.533203 ~ 32.299805
3 = 32.299805 ~ 43.066406
//~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~
2046 = 22028.467 ~ 22039.234
2047 = 22039.234 ~ 22050.0
2048 = 22050.0 ~ 22060.766

3.オーディオ入力された音声をフーリエ変換し、周波数情報をリストに格納

FFTクラスのforwardメソッドで解析を行い、

その結果を自前で作成したFFTSampleFrequencyクラスのリストに格納します。

        public void draw() {
                //~~~~~~~~~~~~~~~~~~~~省略~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

                //オーディオ入力音をフーリエ変換
                fft.forward(audioInput.mix);
                sampleFrequencies = new ArrayList();
                for(int i=0;i<fft.specSize();i++){
                        float hueColor = map(i, 0, fft.specSize(), 0, 180);
                        //周波数の情報をリストに格納
                        sampleFrequencies.add(new FFTSampleFrequency(
                                        i,fft.getBandWidth()*i,fft.getBand(i),hueColor));
                }

4.フーリエ変換の結果を棒グラフで表示

translateメソッドで画面下に移動し、画面の下から上に音圧に応じた長さのラインを描画します。

                        translate(0, height);
                        for (FFTSampleFrequency fftSampleFrequency : sampleFrequencies) {
                                float x = map(fftSampleFrequency.getNo(), 0, 
                                                sampleFrequencies.size(), 0, width);
                                stroke(fftSampleFrequency.getHueColor(),100,100);
                                strokeWeight(2);
                                println(fft.getBand(fftSampleFrequency.getNo()));
                                
                                float high = map(fftSampleFrequency.getPressure(), 
                                                0.0F, 16.0F, 0, height/8);
                                line(x, 0, 
                                                x ,-high);
                        }

上段にサイン波を表示させるため、

グラフの高さは制限しています。

5.最大音圧の周波数を取得

周波数情報のリストより低域、中域、中高域、高域の周波数帯域で最大音圧の周波数を取得します。

 

周波数帯域 下限 上限
低域 0Hz 300Hz
中域 301Hz 2000Hz
中高域 2001Hz 8000Hz
高域 8001Hz 22061Hz
/**
         * 指定したサンプリング周波数を抽出したリストを作成
         * @param frequencies 周波数のリスト
         * @param low  周波数の下限
         * @param high 周波数の上限
         * @return 指定した範囲の周波数リスト
         */
        private List getRangeFrequencies(List frequencies,int low,int high) {
                return
                frequencies.stream()
                .filter(sf -> sf.getFrequency() >= low)
                .filter(sf -> sf.getFrequency() <= high)
                .collect(toList());
        }
        
        /**
         * 周波数のリストを降順でソートし、指定した要素の周波数を返す
         * @param frequencies 周波数のリスト
         * @param 取得したい要素のn番目
         * @return 周波数情報
         */
        private FFTSampleFrequency getFreq(List frequencies,int n) {
                return
                frequencies.stream()
                .sorted(Comparator.comparingDouble(FFTSampleFrequency::getPressure).reversed())
                .collect(toList())
                .get(n)
                ;
        }

6.取得した最大音圧の周波数をSin波でそれぞれ表示

1.周波数の変化を感じやすくするため、基準の周波数を440Hzとする。
2.サイン波の動きをつけるため、位相をframeCountで制御
sin(angle*freq.getFrequency()/440  + frameCount *  0.2F);
3.音圧に応じて周波数の高さを制御
 float  amp = map(freq.getPressure(),  0.0F,  16.0F,  0, maxAmp);
                 amp = (amp > maxAmp)? maxAmp : amp;
                 y = y*amp;
                 vertex(i,y);
                        //--------低域---------------------------
                        List lowFreqes = 
                                        getRangeFrequencies(sampleFrequencies, 0, 300);
                        FFTSampleFrequency freqLow = getFreq(lowFreqes, 1);
                        translate(0, height/8);
                        drawSine(width/4, freqLow,height/8);
        /**
         * サイン波を描画
         * @param point  横幅
         * @param freq   周波数
         * @param maxAmp 振幅の最大
         */
        private void drawSine(int point, FFTSampleFrequency freq,float maxAmp){
                stroke(freq.getHueColor(),100,100);
                beginShape();
                int pointCount = point;
                float angle;
                for(int i=0;i<=pointCount;i++){
                        angle = map(i, 0, pointCount, 0, TWO_PI);
                        float y = sin(angle*freq.getFrequency()/440 + frameCount * 0.2F);
                        float amp = map(freq.getPressure(), 0.0F, 16.0F, 0, maxAmp);
                        amp = (amp > maxAmp)? maxAmp : amp;
                        y = y*amp;
                        vertex(i,y);
                }
                endShape();
        }

コード


package music;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;


import static java.util.stream.Collectors.toList;
import ddf.minim.AudioInput;
import ddf.minim.Minim;
import ddf.minim.analysis.FFT;
import processing.core.PApplet;

public class FFTtoSine extends PApplet{
        Minim minim;
        //オーディオ入力
        AudioInput audioInput;
        //フーリエ変換
        FFT fft;
        //フーリエ変換したサンプル周波数情報
        List sampleFrequencies;

        //フーリエ変換のグラフ表示
        boolean displayFFT = false;
        //サイン波を表示
        boolean displaySineWave = false;
        
        public void settings() {
                fullScreen();
                //画面サイズを出力
                println("fullWidth ::" + width);
                println("fullHeight::" + height);
        }
        
        public void setup() {
                //Minimの初期化
                minim = new Minim(this);
                //ステレオ,バッファサイズ4096でオーディオ入力音を取得するように指定
                audioInput = minim.getLineIn(Minim.STEREO,4096);
                
                //フーリエ変換のFFTオブジェクトを生成
                fft = new FFT(audioInput.bufferSize(), audioInput.sampleRate());
                //周波数帯域を出力
                for(int i = 0; i < fft.specSize(); i++)
                {
                        println(i + " = " + fft.getBandWidth()*i + " ~ "+ fft.getBandWidth()*(i+1));
                }
                //窓関数にハミング窓を指定
                fft.window(FFT.HAMMING);
                
                //HSBカラーモードを指定
                colorMode(HSB,360,100,100,100);
        }
        
        public void draw() {
                background(0);
                stroke(255);
                
                //オーディオ入力音をフーリエ変換
                fft.forward(audioInput.mix);
                sampleFrequencies = new ArrayList();
                for(int i=0;i<fft.specSize();i++){
                        float hueColor = map(i, 0, fft.specSize(), 0, 180);
                        //周波数の情報をリストに格納
                        sampleFrequencies.add(new FFTSampleFrequency(
                                        i,fft.getBandWidth()*i,fft.getBand(i),hueColor));
                }
                
                if(displayFFT){
                        translate(0, height);
                        for (FFTSampleFrequency fftSampleFrequency : sampleFrequencies) {
                                float x = map(fftSampleFrequency.getNo(), 0, 
                                                sampleFrequencies.size(), 0, width);
                                stroke(fftSampleFrequency.getHueColor(),100,100);
                                strokeWeight(2);
                                println(fft.getBand(fftSampleFrequency.getNo()));
                                
                                float high = map(fftSampleFrequency.getPressure(), 
                                                0.0F, 16.0F, 0, height/8);
                                line(x, 0, 
                                                x ,-high);
                        }
                }
                
                if(displaySineWave){
                        resetMatrix();
                        fill(0);
                        stroke(255);
                        //--------低域---------------------------
                        List lowFreqes = 
                                        getRangeFrequencies(sampleFrequencies, 0, 300);
                        FFTSampleFrequency freqLow = getFreq(lowFreqes, 1);
                        translate(0, height/8);
                        drawSine(width/4, freqLow,height/8);
                        //--------中域---------------------------
                        List midFreqes = 
                                        getRangeFrequencies(sampleFrequencies, 301, 2000);
                        FFTSampleFrequency freqMid = getFreq(midFreqes, 1);
                        translate(width/4, 0);
                        drawSine(width/4, freqMid,height/8);
                        //--------中高域--------------------------
                        List midHighFreqes = 
                                        getRangeFrequencies(sampleFrequencies, 2001, 8000);
                        FFTSampleFrequency freqMidHigh = getFreq(midHighFreqes, 1);
                        translate(width/4, 0);
                        drawSine(width/4, freqMidHigh,height/8);
                        //--------高域----------------------------
                        List highFreqes = 
                                        getRangeFrequencies(sampleFrequencies, 8001, 22061);
                        FFTSampleFrequency freqHigh = getFreq(highFreqes, 1);
                        translate(width/4, 0);
                        drawSine(width/4, freqHigh,height/8);

                        //--------全域----------------------------
                        FFTSampleFrequency freqAll = getFreq(sampleFrequencies, 1);
                        resetMatrix();
                        translate(0, height/2);
                        drawSine(width, freqAll,height/4);
                }
                //リストに格納した周波数情報をクリア
                sampleFrequencies.clear();
        }
        
        /**
         * 指定したサンプリング周波数を抽出したリストを作成
         * @param frequencies 周波数のリスト
         * @param low  周波数の下限
         * @param high 周波数の上限
         * @return 指定した範囲の周波数リスト
         */
        private List getRangeFrequencies(List frequencies,int low,int high) {
                return
                frequencies.stream()
                .filter(sf -> sf.getFrequency() >= low)
                .filter(sf -> sf.getFrequency() <= high)
                .collect(toList());
        }
        
        /**
         * 周波数のリストを降順でソートし、指定した要素の周波数を返す
         * @param frequencies 周波数のリスト
         * @param 取得したい要素のn番目
         * @return 周波数情報
         */
        private FFTSampleFrequency getFreq(List frequencies,int n) {
                return
                frequencies.stream()
                .sorted(Comparator.comparingDouble(FFTSampleFrequency::getPressure).reversed())
                .collect(toList())
                .get(n)
                ;
        }
        
        /**
         * サイン波を描画
         * @param point  横幅
         * @param freq   周波数
         * @param maxAmp 振幅の最大
         */
        private void drawSine(int point, FFTSampleFrequency freq,float maxAmp){
                stroke(freq.getHueColor(),100,100);
                beginShape();
                int pointCount = point;
                float angle;
                for(int i=0;i<=pointCount;i++){
                        angle = map(i, 0, pointCount, 0, TWO_PI);
                        float y = sin(angle*freq.getFrequency()/440 + frameCount * 0.2F);
                        float amp = map(freq.getPressure(), 0.0F, 16.0F, 0, maxAmp);
                        amp = (amp > maxAmp)? maxAmp : amp;
                        y = y*amp;
                        vertex(i,y);
                }
                endShape();
        }
        
        public void stop() {
                //Minimを停止
                minim.stop();
                super.stop();
        }
        
        public void keyReleased() {
                
                if(key == 'f' || key == 'F'){
                        displayFFT = !displayFFT;
                }
                if(key == 's' || key == 'S'){
                        displaySineWave = !displaySineWave;
                }
        }
        
        public static void main(String[] args) {
                PApplet.main(music.FFTtoSine.class.getName());
        }
}

package music;

public class FFTSampleFrequency {

        private int no;
        private float frequency;
        private float pressure;
        private float hueColor;

        
        public FFTSampleFrequency(int _no,float _frequncy,float _pressure,float _hueColor) {
                no = _no;
                frequency = _frequncy;
                pressure = _pressure;
                hueColor = _hueColor;
        }
        
        public int getNo() {
                return no;
        }

        public void setNo(int no) {
                this.no = no;
        }

        @Override
        public String toString() {
                return "No" + no + "freq:" + frequency + "\r\n" + "press:" + pressure;
        }

        public float getHueColor() {
                return hueColor;
        }

        public void setHueColor(float hueColor) {
                this.hueColor = hueColor;
        }

        public float getFrequency() {
                return frequency;
        }

        public void setFrequency(float frequency) {
                this.frequency = frequency;
        }

        public float getPressure() {
                return pressure;
        }

        public void setPressure(float pressure) {
                this.pressure = pressure;
        }
        
}