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

FFT

インターホンが鳴ったかどうかを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でインターホンが鳴った時の時間を取得している。

コメント

  1. N.Y. より:

    この記事を参考にさせていただいているものです。とてもありがたいです。一つ質問ですが、この記事で初めて周波数分布を出す時に「ampSpe」というものが出てきます。これまでのプログラム内には一切出てきていないようなのですが、これはなんでしょうか?

  2. dululuttu より:

    N.Y.さん
    “ampSpe”の部分は2行上で定義した”fft_data”とするべき部分でした。
    記事のほうは修正させていただきました。
    ご指摘いただきありがとうございます。

タイトルとURLをコピーしました