FFTでインターホンの音を検知する(Python)

インターホンが鳴ったかどうかをFFT高速フーリエ変換)で調べる。


フーリエ変換とは、ある信号を様々な周波数の正弦波の和として表す考え方。

フーリエ変換FFTに関してはこちらの記事が参考になった。
離散フーリエ変換 - 人工知能に関する断創録


PyAudioでマイクから録音する

まずは、PyAudioを使ってパソコンにつないだUSBマイクから音を録音した。
以下のコードで、インターホンの「ピンポーン」という音を含んだ3秒間の音声を録音し、sound_data.wavというファイルに保存した。

import pyaudio
import numpy as np
import wave

CHUNK = 1024
RATE = 44100
RECORD_TIMES = 3    #3秒間録音
data = []

file_name = "./sound_data.wav"

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
                channels = 1,   #モノラル
                input_device_index = 0,
                rate = RATE,
                frames_per_buffer = CHUNK,
                input = True,
                output = False)

for i in range(int(RATE / CHUNK * RECORD_TIMES)):
    d = np.frombuffer(stream.read(CHUNK), dtype='int16')
    data.append(d)

wf = wave.open(file_name, 'w')
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(RATE)


stream.stop_stream()
stream.close()
p.terminate()

録音した音声の波の形を見てみる

matplotlibを使ってグラフに表示してみる。

import matplotlib.pyplot as plt

wf = wave.open(file, "rb")
data = np.frombuffer(wf.readframes(wf.getnframes()), dtype='int16')
wf.close()

x = np.arange(data.shape[0]) / RATE
plt.plot(x, data)
plt.show()

するとこんなグラフがえられた。
f:id:dululuttu:20181009013334p:plain
横軸は時間、縦軸は振幅を表している。
約0.5秒くらいから「ピンポーン」が始まっている。
これを、FFTして各周波数成分ごとに分けて表示していく。

NumPyのfftパッケージを用いてFFTしていく。

fft_data = np.abs(np.fft.fft(data))    #FFTした信号の強度
freqList = np.fft.fftfreq(data.shape[0], d=1.0/RATE)    #周波数(グラフの横軸)の取得
plt.plot(freqList, fft_data)
plt.xlim(0, 5000)    #0~5000Hzまでとりあえず表示する
plt.show()

f:id:dululuttu:20181009014514p:plain
横軸は周波数(Hz)、縦軸は周波数成分の強度を表している。
グラフの2600Hzと2050Hz付近に大きなピークがみられる。おそらく、これらが「ピーン」と「ポーン」だと思われる。
確認のため、雑音も含めたインターホンの音のグラフも見てみる。
f:id:dululuttu:20181009015232p:plain
このグラフは、インターホンの音とテレビの雑音が含まれている音の波形である。
これを、FFTすると
f:id:dululuttu:20181009015353p:plain
こんな感じになった。やはり、2600Hzと2050Hz付近に大きなピークがみられる。インターホンの周波数はこの2つで間違いないと思う。

インターホンが鳴ったかどうかを判断する

今回は、インターホンが鳴ったらその時の音を3秒間保存する機能を作った。
コードは以下の通り

import pyaudio
import numpy as np
import wave
from datetime import datetime

CHUNK = 1024
RATE = 44100
l = 10 ** 7 
sound_count = 0

data1 = []
data2 = []

freqList = np.fft.fftfreq(int(1.5 * RATE / CHUNK) * CHUNK * 2, d = 1.0 / RATE)

p = pyaudio.PyAudio()
stream = p.open(format = pyaudio.paInt16,
                channels = 1,
                input_device_index = 0,
                rate = RATE,
                frames_per_buffer = CHUNK,
                input = True,
                output = False)

try:
    while stream.is_active():
        for i in range(int(1.5 * RATE / CHUNK)):
            d = np.frombuffer(stream.read(CHUNK), dtype='int16')
            if sound_count == 0:
                data1.append(d)

            else:
                data1.append(d)
                data2.append(d)

        if sound_count >= 1:
            if sound_count % 2 == 1:
                data = np.asarray(data1).flatten()
                fft_data = np.fft.fft(data)
                data1 = []

            else:
                data = np.asarray(data2).flatten()
                fft_data = np.fft.fft(data)
                data2 = []

            fft_abs = np.abs(fft_data)

            data2050 = fft_abs[np.where((freqList < 2150) & (freqList > 2000))]    #2050Hz付近の周波数成分
            data2600 = fft_abs[np.where((freqList < 2700) & (freqList > 2500))]    #2600Hz付近の周波数成分

            if (data2050.max() > 0.5 * l) & (data2600.max() > 1 * l):    #2050Hz付近と2600Hz付近の強度が一定以上あったとき、インターホンが鳴ったと判断

                this_time = datetime.now().strftime("%Y-%m-%d %H-%M-%S ")
                file_name = this_time + ".wav"

                wf = wave.open(file_name, 'w')
                wf.setnchannels(1)
                wf.setsampwidth(2)
                wf.setframerate(RATE)
                wf.writeframes(data)
                wf.close()

                print("The bell is ringing! " + this_time)
                data1 = []
                data2 = []
                sound_count = 0

        sound_count += 1

except KeyboardInterrupt:
    stream.stop_stream()
    stream.close()
    p.terminate()

tryとかexceptはよくわからないので適当。
data1とdata2に分けている理由は3秒間の録音の切れ目で「ピーン」と「ポーン」が分かれてしまい、判定できないことを防ぐため。また、datetimeでインターホンが鳴った時の時間を取得している。