Webカメラを監視し、赤ちゃんが泣いたらGoogle Home (Nest)で泣き声を流す

今月、次女が誕生しました。母子とも健康でありがたい限りです。

赤ちゃんとは別の部屋に寝ているため、赤ちゃんの様子を監視するためWebカメラを導入しました。
カメラには動体検知がついてはいますが、ほぼ動かない新生児には役に立ちません。泣いたら通知が欲しいとは思いますが、音声検知がついているベビーモニターもありますが基本的にはアプリに通知のみなので、スマホの通知に気づかなかったら意味がありません。
ということで、DIYで音声検知機能を付けることにしました。

Webカメラは、
TP-LINK Tapo C200
というものを使っています。プライムセールで安かったから買ってしまったものです。
天井に吊るすように設置しています。
さて、こちらのWebカメラでは撮影している映像をRTSPでライブストリーミングしてくれます。
同じLAN内にあるサーバで、このライブストリーミングを監視して、大きな音を検知したら、その音をGoogle Homeで流す仕組みを作りました。

ライブストリーム動画からの音声の切り出し
ライブストリーム動画からffmpegを使用して、以下のコマンドで5秒間だけ音声をWAVデータで取り出します。

ffmpeg -rtsp_transport tcp -i rtsp://user:pass@192.168.1.10:554/stream2 -vn -acodec copy -t 00:00:05 ./tmp.wav

(CameraのURLを192.168.1.10としています)
これで、音声だけ、tmp.wavに抜き出せました。

wavファイルの解析
さて、このwavファイルを操作して、泣き声を検知することを考えます。
本当にしっかりやりたければ、「FFTなどで周波数ごとの音の強さを調べて、泣き声の特徴とマッチする場合・・」ということをするべきとは思います。
時間があれば、やってみたいとは思いますが、実用的には「大きな音がなっていれば泣き声と判定する」で十分でしたので、全体の音圧のみを調べることにします。
ここでも、ffmpegを使います。

ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1

長い出力が出ますが、必要な音圧は

  • I -70.0 LUFS

というように表示されている部分です。そのためgrepで絞ります。

ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I:
    I:         -70.0 LUFS
[Parsed_ebur128_0 @ 0xca71c0] t: 0.0999792  M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.199979   M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.299979   M:-120.7 S:-120.7     I: -70.0 LUFS     LRA:   0.0 LU
[Parsed_ebur128_0 @ 0xca71c0] t: 0.399979   M: -69.4 S:-120.7     I: -69.4 LUFS     LRA:   0.0 LU
.
.
.

一定時間ごとにその区間の音圧が出力されています。
ここからの処理はシェルスクリプトではやりづらいので、Pythonに流します。

test.py

import sys
import re
result = re.findall('I:\s*([0-9\.\-]*)\s*LUFS', sys.stdin.read(), re.S)
print(result)
ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I: | python test.py

出力

['-70.0', '-70.0', '-70.0', '-70.0', '-69.4', '-69.1', '-49.9', '-45.0', '-43.3', '-42.4', '-42.0', '-42.0', '-41.9', '-41.9', '-40.4', '-38.1', '-36.7', '-35.7', '-35.2', '-35.2', '-35.3', '-35.5', '-35.5', '-35.5', '-35.5', '-35.5', '-35.5', '-35.7', '-36.0', '-36.1', '-36.2', '-36.4', '-36.5', '-36.6', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.8', '-36.6', '-36.4', '-36.1', '-35.9', '-35.9', '-36.0', '-36.1', '-36.1', '-36.2', '-36.1', '-36.1', '-36.2', '-36.2', '-36.3', '-36.3', '-36.3', '-36.3']

というように、音圧の変化を配列として得ることができます。
あとは、Pythonスクリプトの中でこの配列を処理して、大きな音がなっているかを判定します。
環境や検知したい音によって判定方法はいろいろあると思いますが、筆者は上記配列の最大値が-30.0以上のとき、通知をするようにしています。

音が大きいと判断したとき、Google homeで音を流す
今までの処理は、5秒間Webカメラから音を取得して、そのときの音が大きいか判定する処理です。
まず、この処理を定期的に実施する必要があります。
筆者の自宅サーバにはJenkinsが動いていたので、Jenkinsにのせ定期実行しています。(cron等でもいいと思います)
以下Jenkins上で動かす前提で進めます。

音が大きいとき、うえで取得したwavファイルをGoogle Homeで流したいのですが、どうやらwavファイルには対応していなさそうです。(ドキュメントなどを見たわけではないですがwavでは動作はしませんでした)
そのためmp3ファイルに変換します。また、wavファイルはカメラのマイクの質にもよりますが、基本的には音量が小さいので、音圧も一緒にあげます。(どのくらい音量をあげるかは環境にあわせます)
ここも同じくffmpegを使います。

ffmpeg -i "tmp.wav" -vn -af volume=15dB -ar 44100 -acodec libmp3lame -f mp3 "tmp.mp3"

このmp3ファイルを、httpでGoogle Homeからアクセスできる場所に置きます。
mp3をGoogle Homeで流すには、pychromecastを使います(pipでインストール)。

Jenkinsで実行するシェルスクリプトと合わせると、以下のような形になります。
/var/www/html以下がWebサーバで公開される前提
サーバのIPアドレス:192.168.1.50
Google Home:192.168.1.200
Tapo C200:192.168.1.10
python 3.6

rm tmp.* -f
rm /var/www/html/*.mp3 -f
ffmpeg -rtsp_transport tcp -i rtsp://user:pass@192.168.1.10:554/stream2 -vn -acodec copy -t 00:00:05 ./tmp.wav
ffmpeg -i "tmp.wav" -vn -af volume=15dB -ar 44100 -acodec libmp3lame -f mp3 "tmp.mp3"
cp tmp.mp3 
/var/www/html/tmp$BUILD_NUMBER.mp3
ffmpeg -nostats -i tmp.wav -filter_complex ebur128 -f null /dev/null 2>&1 | grep I: | python wavcheck.py tmp$BUILD_NUMBER.mp3

wavcheck.py

import sys
import re
import pychromecast

def play_googlehome(ipaddr, url):
    speaker = pychromecast.Chromecast(ipaddr)
    try:
        if not speaker.is_idle:
            speaker.quit_app()
            time.sleep(5)
        speaker.wait()
        speaker.media_controller.play_media(url, 'audio/mp3')
        speaker.media_controller.block_until_active()
    except Exception as error:
        print(str(error))

result = re.findall('I:\s*([0-9\.\-]*)\s*LUFS', sys.stdin.read(), re.S)
maxv = float(max(result, key=float))
if maxv > -40:
    play_googlehome("192.168.1.200", "http://192.168.1.50/" + sys.argv[1])

一回ごとmp3のファイル名を変えていますが、ファイル名が同じだとキャッシュがきいてしまって、前回と同じ音声が流れてしまうことがあるようでその対策のためです。

まとめ
WAVの解析の部分をもう少しこったことをするとより快適になると思います。
numpyなどを使って、Pythonで音声解析する方法もあるようです。
娘をだっこした状態でブログを書いてきましたが、そろそろしんどくなってきたのでこの辺で・・