West Gate Laboratory

人生を少しでも,面白く便利にするモノづくり

ラズベリーパイでインターホンの音を検知する

前回の記事で、オートロックマンション不在時荷物受け取りシステム(仮)の材料を揃えた。 westgate-lab.hatenablog.com

ここから実際にソフトウェアを開発していく。

やりたいこと

配達員の来訪を検知するには何はともあれインターホンが鳴ったことを検知する必要がある。原理的にはインターホンを分解してどこかからインターホンの信号を拾ってもいいが、賃貸マンションなのであまり原状復帰が難しくなるようなことはしないこととする。

なので、まずはウェブカメラについたマイクでインターホンの音を拾い、インターホンの音を識別、検知することを目標とする。使うウェブカメラは、logicoolのC270だ。

また、ラズベリーパイはModel 3B+である。言語はPythonで組んでいく。

リアルタイムで音声を取得する

ラズパイ+ウェブカメラ+Pythonで手軽に音声データを取得するにはPyAudioが便利である。 PyAudioの使い方は以下のNoteを参考にしたので、そちらを見てほしい。

リアルタイムで音声波形の取得【PyAudio】|もくいち|note

ここのコードをコピペすればとりあえずリアルタイムに音声の時系列データ(一定個数の配列)が得られるはずである。

如何にしてインターホンの音を識別するか

機械学習など使うほうがイマドキ感があるが、まずは目立つ周波数を抜き出して比較することとする。

家のインターホンの音を周波数解析してみる

家のインターホンの音がどういった周波数で構成されているか、スマホで録音したインターホンの音をAudacityのスペクトル解析機能を使って解析した。

うちのインターホンは鳴ると「ピロピロピロピロ・・・」という感じで、「ピ(↑)ロ(↓)」という高低の音の繰り返しである。

音声波形を表示するとこんな感じ。

f:id:kaname_m:20191225221201p:plain
我が家のインターホン音声(一部)

最初が大きく、徐々に小さくなり、それが繰り返されることがわかる。

では、次にピ(↑)、すなわち高音部分を抜き出してスペクトル解析する。

f:id:kaname_m:20191225221204p:plain
高音部分のスペクトル解析結果

いくつも波が立っているが、インターホンの音に対応するピークは基音から886Hz, 1778Hz, 2665Hz, 3550Hz・・・となっており、キレイに倍音が出ていることがわかる。

次にロ(↓)、すなわち低音部分を抜き出してスペクトル解析する。

f:id:kaname_m:20191225221208p:plain
低音部分のスペクトル解析結果

同様にピークが立っており、基音から727Hz, 1450Hz, 2176Hz, 2905Hz・・・となっている。こちらもきれいな倍音である。うちのインターホンは、低音も高音も3倍音が最も大きい周波数のようである。

高速フーリエ変換FFT)でインターホンの音を識別する

インターホンの音の素性がわかればあとはラズベリーパイでFFTしてその周波数を検知するのみである。

FFTはnumpyを使った。

手順としては、

  1. 常にPyAudioで音声を取得
  2. FFTで最大振幅の周波数を取得
  3. その周波数が低音の基音、2倍音、3倍音・・・の周辺であればインターホンの低音と識別
  4. その周波数が高音の基音、2倍音、3倍音・・・の周辺であればインターホンの高音と識別
  5. 高音・低音を検知したらインターホンが鳴ったものとする。

手順3,4でスペクトル解析で最も大きい音が出ていた3倍音以外も取ることにしたのは、録音環境やウェブカメラのマイクのスペックにより微妙に振幅が変化するためである。

コードはこんな感じ。

import pyaudio
import numpy as np
import time
from scipy.signal import argrelmax

# interphone setting
CHUNK = 1024
RATE = 8000    # sampling rate
dt = 1/RATE
freq = np.linspace(0,1.0/dt,CHUNK)
fn = 1/dt/2;    # nyquist freq
FREQ_HIGH_BASE = 886.0  # high tone frequency
FREQ_LOW_BASE = 726.6   # low tone frequency
FREQ_ERR = 0.02         # allowable freq error
#variable
detect_high = False
detect_low = False

# FFTで振幅最大の周波数を取得する関数
def getMaxFreqFFT(sound, chunk, freq):
    # FFT
    f = np.fft.fft(sound)/(chunk/2)
    f_abs = np.abs(f)
    # ピーク検出
    peak_args = argrelmax(f_abs[:(int)(chunk/2)])
    f_peak = f_abs[peak_args]
    f_peak_argsort = f_peak.argsort()[::-1]
    peak_args_sort = peak_args[0][f_peak_argsort]
    # 最大ピークをreturn
    return freq[peak_args_sort[0]]

# 検知した周波数がインターホンの音の音か判定する関数
def detectDualToneInOctave(freq_in, freq_high_base, freq_low_base, freq_err):
    det_h = det_l = False
    # 検知した周波数が高音・低音のX倍音なのか調べる
    octave_h = freq_in / freq_high_base
    octave_l = freq_in / freq_low_base
    near_oct_h = round(octave_h)
    near_oct_l = round(octave_l)
    if near_oct_h == 0 or near_oct_l == 0:
        return False, False
    # X倍音のXが整数からどれだけ離れているか
    err_h = np.abs((octave_h-near_oct_h) / near_oct_h)
    err_l = np.abs((octave_l-near_oct_l) / near_oct_l)

    # 基音、2倍音、3倍音の付近であればインターホンの音とする
    if err_h < freq_err:
        det_h = True
    elif err_l < freq_err:
        det_l = True

    return det_h, det_l

if __name__=='__main__':
    P = pyaudio.PyAudio()
    stream = P.open(format=pyaudio.paInt16, channels=1, rate=RATE, frames_per_buffer=CHUNK, input=True, output=False)
    
    while stream.is_active():
        try:
            input = stream.read(CHUNK, exception_on_overflow=False)
            ndarray = np.frombuffer(input, dtype='int16')
            abs_array = np.abs(ndarray)/32768
        
            if abs_array.max() > 0.5:
                # FFTで最大振幅の周波数を取得
                freq_max = getMaxFreqFFT(ndarray, CHUNK, freq)
                print("振幅最大の周波数:", freq_max, "Hz")
                h,l = detectDualToneInOctave(freq_max, FREQ_HIGH_BASE, FREQ_LOW_BASE, FREQ_ERR)
                if h:
                    detect_high = True
                    print("高音検知!")
                if l:
                    detect_low = True
                    print("低音検知!")
    
                # dual tone detected
                if detect_high and detect_low:
                    print("インターホンの音を検知!")
                    time.sleep(30)
                    print("フラグリセット")
                    detect_high = detect_low = False
    
        except KeyboardInterrupt:
            break
        
    stream.stop_stream()
    stream.close()
    P.terminate()

実際にpython3で起動させて、インターホンの音をウェブカメラに聞かせると、

振幅最大の周波数: 2674.4868035190616 Hz
高音検知!
振幅最大の周波数: 2189.6383186705766 Hz
低音検知!
インターホンの音を検知!

と出力され、正しくインターホンの音が検知できたことを確認できる。

ただし、最大振幅の周波数を見ているだけなので、日常生活のノイズをたまーにインターホンの音と誤検知する場合がある(週に2,3回)。これは将来的に機械学習で対応したいと思う。