Android:通过fft获取更精确的频率

Posted

技术标签:

【中文标题】Android:通过fft获取更精确的频率【英文标题】:Android: Get more precise frequencies through fft 【发布时间】:2015-09-10 15:37:43 【问题描述】:

我正在使用this FFTBasedSpectrumAnalyzer 来分析麦克风收集的声音。但是,FFTBasedSpectrumAnalyzer 创建了一个图表,而我想要一个可以放在标签中的频率,所以我试图通过这个公式获得峰值的频率:mFreq = (((1.0 * frequency) / (1.0 * blockSize)) * mPeakPos)/2。我也通过这个公式得到幅度(以及峰值和峰值频率):

int mPeakPos = 0;
                double mMaxFFTSample = 150.0;
                for (int i = 0; i < progress[0].length; i++) 
                    int x = i;
                    int downy = (int) (150 - (progress[0][i] * 10));
                    int upy = 150;
                    //Log.i("SETTT", "X: " + i + " downy: " + downy + " upy: " + upy);

                    if(downy < mMaxFFTSample)
                    
                        mMaxFFTSample = downy;
                        //mMag = mMaxFFTSample;
                        mPeakPos = i;
                    
                

但是,我有两个问题。首先,最大频率降低了 10-40 Hz,并且即使我播放恒定的音调也会发生变化。其次,我只能分析高达 4000 Hz 的音频。有没有办法让它更准确和/或分析高达 22 kHz 的音频?也许通过将块大小编辑为 256 以外的值或 8000 以外的频率(即使当我尝试这样做时,mFreq 下降到 0 并且 mMaxFFTSample 通常变为 -2)。 谢谢。

完整代码如下:

public class FrequencyListener extends AppCompatActivity 
    private double mFreq;
    private double mMag;
    private boolean mDidHitTargetFreq;
    private View mBackgroundView;

    int frequency = 8000;
    int channelConfiguration = AudioFormat.CHANNEL_IN_MONO;
    int audioEncoding = AudioFormat.ENCODING_PCM_16BIT;

    AudioRecord audioRecord;
    private RealDoubleFFT transformer;
    int blockSize;
    boolean started = false;
    boolean CANCELLED_FLAG = false;


    RecordAudio recordTask;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
        super.onCreate(savedInstanceState);
        blockSize = 256;
        transformer = new RealDoubleFFT(blockSize);

        started = true;
        CANCELLED_FLAG = false;
        recordTask = new RecordAudio();
        recordTask.execute();
    

    private class RecordAudio extends AsyncTask<Void, double[], Boolean> 

        @Override
        protected Boolean doInBackground(Void... params) 

            int bufferSize = AudioRecord.getMinBufferSize(frequency,
                    channelConfiguration, audioEncoding);
            audioRecord = new AudioRecord(
                    MediaRecorder.Audiosource.DEFAULT, frequency,
                    channelConfiguration, audioEncoding, bufferSize);
            int bufferReadResult;
            short[] buffer = new short[blockSize];
            double[] toTransform = new double[blockSize];
            try 
                audioRecord.startRecording();
             catch (IllegalStateException e) 
                Log.e("Recording failed", e.toString());

            
            while (started) 
                if (isCancelled() || (CANCELLED_FLAG == true)) 

                    started = false;
                    //publishProgress(cancelledResult);
                    Log.d("doInBackground", "Cancelling the RecordTask");
                    break;
                 else 
                    bufferReadResult = audioRecord.read(buffer, 0, blockSize);

                    for (int i = 0; i < blockSize && i < bufferReadResult; i++) 
                        toTransform[i] = (double) buffer[i] / 32768.0; // signed 16 bit
                    

                    transformer.ft(toTransform);

                    publishProgress(toTransform);

                

            
            return true;
        
        @Override
        protected void onProgressUpdate(double[]...progress) 

            int mPeakPos = 0;
            double mMaxFFTSample = 150.0;
            for (int i = 0; i < progress[0].length; i++) 
                int x = i;
                int downy = (int) (150 - (progress[0][i] * 10));
                int upy = 150;
                //Log.i("SETTT", "X: " + i + " downy: " + downy + " upy: " + upy);

                if(downy < mMaxFFTSample)
                
                    mMaxFFTSample = downy;
                    //mMag = mMaxFFTSample;
                    mPeakPos = i;
                
            

            mFreq = (((1.0 * frequency) / (1.0 * blockSize)) * mPeakPos)/2;
            Log.i("SETTT", "FREQ: " + mFreq + " MAG: " + mMaxFFTSample);

        
        @Override
        protected void onPostExecute(Boolean result) 
            super.onPostExecute(result);
            try
                audioRecord.stop();
            
            catch(IllegalStateException e)
                Log.e("Stop failed", e.toString());

            
        
    

    @Override
    protected void onPause() 
        super.onPause();
        started = false;
    

    @Override
    protected void onResume() 
        super.onResume();
        started = true;
    

【问题讨论】:

【参考方案1】:

数字信号可以表示的最大频率始终为采样率/2。这被称为Nyquist frequency。如果您需要测量超过 4kHz 的信号,那么唯一可能的解决方案就是提高采样率。

下一个问题是 FFT 的频率分辨率,它是 FFT 大小和采样率的函数。

 binWidthInHz = sampleRate / numBins;

在您的情况下,您的 sampleRate 为 8000 和 256 个 bin,因此每个 bin 的宽度为 31.25 Hz。提高分辨率的唯一方法是 a) 降低采样率或 b) 增加 fft 大小。

最后一点。您似乎没有对信号应用任何窗口。结果是由于spectral leakage,您的峰值将被抹掉。将诸如Hann function 之类的窗口函数应用于您的时域信号将抵消这一点。本质上,FFT 算法通过将信号的副本连接在一起来将信号视为无限长。除非您的信号满足某些条件,否则缓冲区的最后一个样本和第一个样本之间很可能会有很大的跳跃。窗口函数在缓冲区的开始和结束处应用一个锥度来平滑它。

【讨论】:

因此,如果我将频率从 8,000 增加到 40,000,我将能够检测到更高的频率,但它们的准确度会低得多。我试图将频率从 8,000 更改为 4,000,但出现错误:Invalid audio buffer size 在这部分代码:int bufferSize = AudioRecord.getMinBufferSize(frequency, channelConfiguration, audioEncoding); audioRecord = new AudioRecord( MediaRecorder.AudioSource.DEFAULT, frequency, channelConfiguration, audioEncoding, bufferSize); 是的,没错。但你总是可以增加 FFT 大小来补偿。关于错误,我猜 40000 是不受支持的速率,getMinBufferSize 正在返回一个您没有检查的错误。试试 44100 或 48000。 为什么在 iOS 上我设法编写了相同的程序,但它检测到高达 21kHz 的频率,同时保持高水平的准确性?通过增加 bin 的数量和采样率?出于某种原因,这不起作用——我得到的频率都是 0 或接近 0。如果我必须满足于大采样率或高精度水平,有没有办法让我在 2 下获得高精度千赫?这是否只是降低了采样率并以某种方式避免了我刚刚收到的错误? 如果您有较高的准确度和较高的最大频率,那么采样率必须很高,并且您必须有足够的 bin。纯物理。是的,降低采样率以提高低频分辨率的做法并不少见。例如,您可以保持 8kHz 采样率,将缓冲区大小更改为 2048,并具有 3.9Hz 的分辨率。 p.s.我不知道为什么认为您需要使用getMinBufferSize。只需为您的 fft 选择您喜欢的尺寸。 啊。看来我使用的是不受支持的号码。例如,40000 而不是 44100。谢谢。【参考方案2】:

为了提高您可以分析的最大频率,您需要将采样频率提高到您希望分析的最高频率的两倍(奈奎斯特频率)。

为了提高频率分辨率,您可以通过更长时间地观察信号来增加您拥有的时域样本数。您可以选择通过填充输入在频率区间之间进行插值,而不是提高分辨率在执行 FFT 之前或在 FFT 之后的频率区间之间执行某种插值。(有关零填充和频率分辨率的更多信息,请参阅this post。

【讨论】:

以上是关于Android:通过fft获取更精确的频率的主要内容,如果未能解决你的问题,请参考以下文章

使用 EZAudio 从 FFT 获得更精确的频率?

精确的音调开始/持续时间测量?

使用帧之间的相位变化从 FFT Bins 中提取精确频率

如何实时获取特定频率音频信号的幅度android

如何将 FFT 应用于录音以获取频率?

Android音频FFT使用audiorecord检索特定频率幅度