从PyAudio接收到的数据FFT给出了错误的频率

时间:2019-02-08 12:10:28

标签: python signal-processing pyaudio audio-processing

我的主要任务是实时识别人从麦克风发出的嗡嗡声。一般来说,作为识别信号的第一步,我记录了从手机上的应用程序生成的440 Hz信号的5秒钟,并尝试检测相同的频率。

我使用Audacity绘制并验证了来自相同440Hz wav文件的频谱,我得到了这一结果,这表明440Hz实际上是主要频率: (https://i.imgur.com/2UImEkR.png

要使用python做到这一点,我使用PyAudio库并引用this blog。到目前为止,我使用wav文件运行的代码是:

"""PyAudio Example: Play a WAVE file."""

import pyaudio
import wave
import sys
import struct
import numpy as np
import matplotlib.pyplot as plt

CHUNK = 1024

if len(sys.argv) < 2:
    print("Plays a wave file.\n\nUsage: %s filename.wav" % sys.argv[0])
    sys.exit(-1)

wf = wave.open(sys.argv[1], 'rb')

p = pyaudio.PyAudio()
stream = p.open(format=p.get_format_from_width(wf.getsampwidth()),
                channels=wf.getnchannels(),
                rate=wf.getframerate(),
                output=True)

data = wf.readframes(CHUNK)

i = 0
while data != '':
    i += 1
    data_unpacked = struct.unpack('{n}h'.format(n= len(data)/2 ), data) 
    data_np = np.array(data_unpacked) 
    data_fft = np.fft.fft(data_np)
    data_freq = np.abs(data_fft)/len(data_fft) # Dividing by length to normalize the amplitude as per https://www.mathworks.com/matlabcentral/answers/162846-amplitude-of-signal-after-fft-operation
    print("Chunk: {} max_freq: {}".format(i,np.argmax(data_freq)))

    fig = plt.figure()
    ax = fig.add_subplot(1,1,1)
    ax.plot(data_freq)
    ax.set_xscale('log')
    plt.show()

    stream.write(data)
    data = wf.readframes(CHUNK)

stream.stop_stream()
stream.close()

p.terminate()

在输出中,我得到所有块的最大频率为10,其中一个图的示例为: (https://i.imgur.com/zsAXME5.png

我本来希望所有块的这个值是440,而不是10。我承认我对FFT的理论了解甚少,也感谢我为解决这个问题所提供的帮助。

编辑: 采样率为44100。的通道数是2,样本宽度也是2。

1 个答案:

答案 0 :(得分:0)

前言

正如xdurch0所指出的那样,您正在读取一种索引而不是频率。如果要自己进行所有计算,则在绘制图形之前,如果要获得一致的结果,则需要计算自己的频率向量。阅读此answer可能有助于您找到解决方案。

FFT(半平面)的频率向量为:

 f = np.linspace(0, rate/2, N_fft/2)

或(全平面):

 f = np.linspace(-rate/2, rate/2, N_fft)

另一方面,我们可以将大部分工作委托给出色的scipy.signal工具箱,该工具箱旨在解决此类问题(以及更多问题)。

MCVE

使用scipy包,可以很容易地为单一频率(source)的简单WAV文件获得所需的结果:

import numpy as np
from scipy import signal
from scipy.io import wavfile
import matplotlib.pyplot as plt

# Read the file (rate and data):
rate, data = wavfile.read('tone.wav') # See source

# Compute PSD:
f, P = signal.periodogram(data, rate) # Frequencies and PSD

# Display PSD:
fig, axe = plt.subplots()
axe.semilogy(f, P)
axe.set_xlim([0,500])
axe.set_ylim([1e-8, 1e10])
axe.set_xlabel(r'Frequency, $\nu$ $[\mathrm{Hz}]$')
axe.set_ylabel(r'PSD, $P$ $[\mathrm{AU^2Hz}^{-1}]$')
axe.set_title('Periodogram')
axe.grid(which='both')

基本上:

这将输出:

enter image description here

发现峰

然后,我们可以使用find_peaks找到第一个最高峰的频率(P>1e-2,此准则可能需要调整):

idx = signal.find_peaks(P, height=1e-2)[0][0]
f[idx] # 440.0 Hz

将所有内容放在一起只能归结为:

def freq(filename, setup={'height': 1e-2}):
    rate, data = wavfile.read(filename)
    f, P = signal.periodogram(data, rate)
    return f[signal.find_peaks(P, **setup)[0][0]]

处理多个渠道

  

我在我的wav文件中尝试了此代码,并得到该行的错误   axe.semilogy(f,Pxx_den)如下:ValueError:x和y必须具有   相同的第一维度。我检查了形状,f有(2,)   Pxx_den具有(220160,2)。另外,Pxx_den数组似乎拥有全部   仅零。

Wav file可以容纳多个通道,主要是单声道或立体声文件(最多2**16 - 1个通道)。您强调的问题是由于多个频道文件(stereo sample)引起的。

rate, data = wavfile.read('aaaah.wav') # Shape: (46447, 2), Rate: 48 kHz

enter image description here

它的文档尚不完善,但是方法signal.periodogram也在矩阵上执行,其输入与wavfile.read的输出不直接相符(默认情况下,它们在不同的轴上执行)。因此,在执行PSD时,我们需要仔细调整尺寸方向(使用axis开关:

f, P = signal.periodogram(data, rate, axis=0, detrend='linear')

它也可以与转置data.T一起使用,但是我们需要向后转置结果。

指定轴可解决问题:频率矢量正确且PSD并非在所有位置都为空(在长度为axis=1的{​​{1}}上执行之前,在您的情况下,它在2上执行了220160 PSD -采样我们想要相反的信号。

2开关可确保信号均值为零,并且线性趋势已消除。

实际应用

只要块中包含足够的数据(请参见Nyquist-Shannon sampling theorem),该方法应适用于实际的块状样本。然后,数据是信号(块)的子样本,并且速率在此过程中不会改变,因此保持恒定。

具有detrend大小的块似乎有效,我们可以从中识别出特定的频率:

2**10

enter image description here

这时,最棘手的部分是对f, P = signal.periodogram(data[:2**10,:], rate, axis=0, detrend='linear') # Shapes: (513,) (513, 2) idx0 = signal.find_peaks(P[:,0], threshold=0.01, distance=50)[0] # Peaks: [46.875, 2625., 13312.5, 16921.875] Hz fig, axe = plt.subplots(2, 1, sharex=True, sharey=True) axe[0].loglog(f, P[:,0]) axe[0].loglog(f[idx0], P[idx0,0], '.') # [...] 方法的微调以捕获所需的频率。您可能需要考虑对信号进行预滤波或对PSD进行后处理,以便于识别。