【RaspberryPi】Hot Word検出SnowboyでオリジナルのHot Wordを検知してみる(前編:設定)

今回は、以前のエントリでも挑戦してみたいと思っていた、音声のホットワードを検知するSnowboyをインストールして、トリガー検知を行ってみたいと思います。

f:id:ueponx:20201026022104p:plain

使用するSnowboyは以下のような説明になっていました。

Snowboy is an highly customizable hotword detection engine that is embedded real-time and is always listening (even when off-line) compatible with Raspberry Pi, (Ubuntu) Linux, and Mac OS X.

日本語訳すると…

Snowboyは高度にカスタマイズ可能なホットワード検出エンジンであり、リアルタイムに組み込まれ、Raspberry Pi、(UbuntuLinux、およびMac OS Xと互換性があります(オフラインの場合でも)。

snowboy.kitt.ai

ホットワードとは、Google Homeでいえば「OK、Google」、Alexaでいえば「Alexa」というアレです。Snowboyを使用するとこのホットワードを任意のものに変換することでできるようにできます。あとオフラインでの認識ができるので、ネットワークを使わない分、高速に動作させることもできます。しかもRaspberryPiでも動作できるのでライトウエイトな実装でもあります。

以下のエントリーを参考にさせていただきました。リリースの時期が結構古いので動作環境も変わっているので、ちょっと注意が必要かもしれません。

qiita.com

nyabot.hatenablog.com

今回の作業手順

手順としては以下のようになります。

  1. マイク接続とAlsaの設定
  2. snowboyのインストール
  3. ホットワードのカスタマイズ

使用したRaspberryPiのバージョン

まずは今回の動作環境の確認をしておきます。

RaspberryPiのハードウエアはRaspberryPi 3Bを使用します。 そして、OSのバージョンは次のようになります。(Raspbian GNU/Linux 10 (buster) 10.6)

$ cat /proc/device-tree/model
Raspberry Pi 3 Model B Rev 1.2

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:        10
Codename:       buster

$ cat /etc/debian_version 
10.6

この環境で進めていきます。

マイク接続とAlsaの設定

音声認識によるホットワード検知となるので、マイクは必須となります。今回は以下のマイクをRaspberryPiに接続しました。

MM-MCUSB16

www.sanwa.co.jp

この製品はすでにディスコンになっているので、今だと以下が近い感じでしょうか。ミュートボタンが追加されているのでこちらのほうが便利かも?

接続すると自動的に認識されると思います。lsusbコマンドで表示すると…Bus 001 Device 004: ID 0d8c:0134 C-Media Electronics, Inc.と表示されているのがわかります。

$ lsusb
Bus 001 Device 004: ID 0d8c:0134 C-Media Electronics, Inc. 
Bus 001 Device 003: ID 0424:ec00 Standard Microsystems Corp. SMSC9512/9514 Fast Ethernet Adapter
Bus 001 Device 002: ID 0424:9514 Standard Microsystems Corp. SMC9514 Hub
Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub

ちなみに音声の確認(スピーカー出力)はピンジャックのイヤホンを使用しています。(後でこれがトラブルのもとになります。)

あとはソフトウエア(ALSA)の設定となります。

サウンドバイスの確認と設定変更

サウンドカードの状態を確認するにはaplayarecordコマンドを使用します。以下のように実行すると現在の状態をリスト化して表示してくれます。 実行すると以下のように表示されます。

$ aplay -l
**** ハードウェアデバイス PLAYBACK のリスト ****
カード 0: Headphones [bcm2835 Headphones], デバイス 0: bcm2835 Headphones [bcm2835 Headphones]
  サブデバイス: 8/8
  サブデバイス #0: subdevice #0
  サブデバイス #1: subdevice #1
  サブデバイス #2: subdevice #2
  サブデバイス #3: subdevice #3
  サブデバイス #4: subdevice #4
  サブデバイス #5: subdevice #5
  サブデバイス #6: subdevice #6
  サブデバイス #7: subdevice #7

$ arecord -l
**** ハードウェアデバイス CAPTURE のリスト ****
カード 1: Device [USB PnP Audio Device], デバイス 0: USB Audio [USB Audio]
  サブデバイス: 1/1
  サブデバイス #0: subdevice #0

今回のRaspberryPiはヘッドレスにしているので、HDMIバイスは再生デバイスとしては表示されていません。設定時にはどちらにするか必ず確認するようにしてください。カード情報のどこかにHDMIと入っているものが該当します。

f:id:ueponx:20201024222109p:plain

今回は再生デバイスはカード0デバイス0を、録音デバイスはカード1デバイス0のものを使用していきます。こちらを使用するように~/.asoundrcを編集していきます。vinanoなどでファイルを下記のように生成します。設定での注意としてはslave.pcmの値をカード0デバイス0の場合にはhw:0,0というように変更していきます。

~/.asoundrc

pcm.!default {
  type asym
   playback.pcm {
     type plug
     slave.pcm "hw:0,0"
   }
   capture.pcm {
     type plug
     slave.pcm "hw:1,0"
   }
}

これでALSAでのサウンドバイスの設定変更は完了です。 設定がうまく行っているかを確認する場合には以下のコマンドを実行してみて、録音、再生ができていれば大丈夫です。

# マイクからの音声の録音
$ rec test.wav
# スピーカーから録音した音声ファイルの再生
$ play test.wav

再生しても、録音した内容が聞こえないのであれば、もう一度設定ファイルの中身を確認してみてください。

Snowboyのインストール

SnowboyGitHubからgit cloneでソースを取得してインストールを行います。

github.com

gitを使わない場合には以下を使用しても大丈夫です。

https://s3-us-west-2.amazonaws.com/snowboy/snowboy-releases/rpi-arm-raspbian-8.0-1.3.0.tar.bz2

$ git clone https://github.com/Kitt-AI/snowboy.git
Cloning into 'snowboy'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 2148 (delta 0), reused 1 (delta 0), pack-reused 2145
Receiving objects: 100% (2148/2148), 55.22 MiB | 2.27 MiB/s, done.
Resolving deltas: 100% (1072/1072), done.
Checking out files: 100% (313/313), done.

GitHubgit cloneを行うとカレントディレクトリにsnowboyというディレクトリが作成されていると思います。ディレクトリ構成は以下のようになっていると思います。

$ cd snowboy/
$ ls
LICENSE      README.md        README_commercial.md  examples  lib           resources  setup.py  tsconfig.json
MANIFEST.in  README_ZH_CN.md  binding.gyp           include   package.json  scripts    swig

ソースが取得出来たら、依存するパッケージをインストールしていきます。 念のためpython3でpyaudioを使用できるように追加してみました。さすがにそろそろpython2系の使用はやめたい感じです。

$ sudo apt update
$ sudo apt install swig3.0 python-pyaudio python3-pyaudio sox
$ pip install pyaudio
$ pip3 install pyaudio
$ sudo apt install libatlas-base-dev

Snowboyで使用するSharedObject(soファイル)を生成するため、GitHubで取得したファイルからmakeを行います。 以下のように行えばmakeができるはずなのですが、ビルドエラーが発生します。

$ cd ~/snowboy/swig/Python3  
$ make

エラーは次の様になっています。swigという名前のコマンドがないためです。aptswigをインストールすると、コマンド名がswig3.0となっているのが原因のようです。 ちなみにswigソースコードからインストールすると、実行ファイル名が単なるswigとなるので、エラーは発生しません。

/bin/sh: 1: swig: not found
expr: syntax error: unexpected argument `30010'
swig -I../../ -c++ -python -o snowboy-detect-swig.cc snowboy-detect-swig.i
make: swig: コマンドが見つかりませんでした
make: *** [Makefile:67: snowboy-detect-swig.cc] エラー 127

仕方ないのでmakefileswigコマンドの指定部分を修正することで対応していきます。

$ vi Makefile 

修正前Makefile

# Example Makefile that converts snowboy c++ library (snowboy-detect.a) to
# python3 library (_snowboydetect.so, snowboydetect.py), using swig.

# Please use swig-3.0.10 or up.
SWIG := swig

(以下略)

修正後Makefile

# Example Makefile that converts snowboy c++ library (snowboy-detect.a) to
# python3 library (_snowboydetect.so, snowboydetect.py), using swig.

# Please use swig-3.0.10 or up.
SWIG := swig3.0

(以下略)

インストールが終わったら、以下のように実行します。(今回はpython3系で実行しています)実行時の引数として音声認識モデルをソースコードに同梱されていたsnowboy.umdlを使用しています。これはスノーボーイという呼びかけ(Hot Word)を検知します。実行後にHot Wordを検知状態になり、検知すると音が鳴るというサンプルになります。

$ cd ~/snowboy/examples/Python3
$ python3 demo.py resources/models/snowboy.umdl 

これで実行できると思いきや…以下のようなエラーが

$ python3 demo.py ./resources/models/snowboy.umdl 
Traceback (most recent call last):
  File "demo.py", line 1, in <module>
    import snowboydecoder
  File "/home/pi/snowboy/examples/Python3/snowboydecoder.py", line 5, in <module>
    from . import snowboydetect
ImportError: attempted relative import with no known parent package

snowboydecoderのインポート(5行目)がおかしいというエラーメッセージになります。では該当するsnowboydecoder.pyの中をみると以下のようになっているので 5行目のfrom . import snowboydetectimport snowboydetectに変更してみます。

$ vi snowboydecoder.py 

(変更前)snowboydecoder.py冒頭

#!/usr/bin/env python

import collections
import pyaudio
from . import snowboydetect
import time
import wave
import os
import logging
from ctypes import *
from contextlib import contextmanager
(略)

(変更後)snowboydecoder.py冒頭

#!/usr/bin/env python

import collections
import pyaudio
import snowboydetect
import time
import wave
import os
import logging
from ctypes import *
from contextlib import contextmanager
(略)

実行するとかなり大量のWarningが表示されるのですが、一応実行できます。表示されるのはALSAのバージョンによるものだと思います。

実行ログ

$ python3 demo.py resources/models/snowboy.umdl 
Listening... Press Ctrl+C to exit
Expression 'alsa_snd_pcm_hw_params_set_period_size_near( pcm, hwParams, &alsaPeriodFrames, &dir )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 924
Expression 'alsa_snd_pcm_hw_params_set_period_size_near( pcm, hwParams, &alsaPeriodFrames, &dir )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 924
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.front.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM front
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.rear
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.center_lfe
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.side
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround51.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround21
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround51.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround21
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround40.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround40
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround51.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround41
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround51.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround50
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround51.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround51
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.surround71.0:CARD=0'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM surround71
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.iec958.0:CARD=0,AES0=4,AES1=130,AES2=0,AES3=2'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM iec958
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.iec958.0:CARD=0,AES0=4,AES1=130,AES2=0,AES3=2'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM spdif
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'cards.bcm2835_headpho.pcm.iec958.0:CARD=0,AES0=4,AES1=130,AES2=0,AES3=2'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5047:(snd_config_expand) Evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM spdif
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.hdmi
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.modem
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM cards.pcm.phoneline
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'defaults.bluealsa.device'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5036:(snd_config_expand) Args evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM bluealsa
ALSA lib confmisc.c:1281:(snd_func_refer) Unable to find definition 'defaults.bluealsa.device'
ALSA lib conf.c:4568:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory
ALSA lib conf.c:5036:(snd_config_expand) Args evaluate error: No such file or directory
ALSA lib pcm.c:2565:(snd_pcm_open_noupdate) Unknown PCM bluealsa
Expression 'alsa_snd_pcm_hw_params_set_period_size_near( pcm, hwParams, &alsaPeriodFrames, &dir )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 924
Cannot connect to server socket err = No such file or directory
Cannot connect to server request channel
jack server is not running or cannot be started
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
JackShmReadWritePtr::~JackShmReadWritePtr - Init not done for -1, skipping unlock
INFO:snowboy:Keyword 1 detected at time: 2020-10-20 23:33:33
Expression 'paInvalidSampleRate' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2048
Expression 'PaAlsaStreamComponent_InitialConfigure( &self->playback, outParams, self->primeBuffers, hwParamsPlayback, &realSr )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2722
Expression 'PaAlsaStream_Configure( stream, inputParameters, outputParameters, sampleRate, framesPerBuffer, &inputLatency, &outputLatency, &hostBufferSizeMode )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 2843
Traceback (most recent call last):
  File "demo.py", line 33, in <module>
    sleep_time=0.03)
  File "/home/pi/snowboy/examples/Python3/snowboydecoder.py", line 221, in start
    callback()
  File "/home/pi/snowboy/examples/Python3/snowboydecoder.py", line 71, in play_audio_file
    rate=ding_wav.getframerate(), input=False, output=True)
  File "/usr/lib/python3/dist-packages/pyaudio.py", line 750, in open
    stream = Stream(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/pyaudio.py", line 441, in __init__
    self._stream = pa.open(**arguments)
OSError: [Errno -9997] Invalid sample rate

ただ、呼びかけてもエラーが発生します。エラー内容は

Traceback (most recent call last):
  File "demo.py", line 33, in <module>
    sleep_time=0.03)
  File "/home/pi/snowboy/examples/Python3/snowboydecoder.py", line 221, in start
    callback()
  File "/home/pi/snowboy/examples/Python3/snowboydecoder.py", line 71, in play_audio_file
    rate=ding_wav.getframerate(), input=False, output=True)
  File "/usr/lib/python3/dist-packages/pyaudio.py", line 750, in open
    stream = Stream(self, *args, **kwargs)
  File "/usr/lib/python3/dist-packages/pyaudio.py", line 441, in __init__
    self._stream = pa.open(**arguments)
OSError: [Errno -9997] Invalid sample rate

Hot Wordを検知したときに鳴らす音の出力時にエラーが出ているようです。そこで先ほど設定した~/.asoundを変更してみました。変更したのは出力(再生)のサンプリングレートを固定したslave.rate 16000という部分になります。

修正後の~/.asound

pcm.!default {
  type asym
   playback.pcm {
     type plug
     slave.pcm "hw:0,0"
     slave.rate 16000
   }
   capture.pcm {
     type plug
     slave.pcm "hw:1,0"
   }
}

設定変更後に実行を行うと問題なくHot Wordを検知し、認識音が鳴るになりました。

$ cd ~/snowboy/examples/Python3

$ python3 demo.py ./resources/models/snowboy.umdl 
Listening... Press Ctrl+C to exit
Expression 'alsa_snd_pcm_hw_params_set_period_size_near( pcm, hwParams, &alsaPeriodFrames, &dir )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 924
Expression 'alsa_snd_pcm_hw_params_set_period_size_near( pcm, hwParams, &alsaPeriodFrames, &dir )' failed in 'src/hostapi/alsa/pa_linux_alsa.c', line: 924
(中略)
INFO:snowboy:Keyword 1 detected at time: 2020-10-24 22:47:29
INFO:snowboy:Keyword 1 detected at time: 2020-10-24 22:47:32

エラーは解消しませんが、無事に動作するようになりました。

ちなみに

違う再生デバイス(USBスピーカー)で確認してみました。

スイッチサイエンスさんで購入可能なUSB接続ミニステレオスピーカーを再生デバイスとしてしようしてみました。これコンパクトでいいんですよね。かなりおすすめです。音量の操作が手元でできないのでその部分だけが少しネックですが。

www.switch-science.com

実際に動作させると先ほどのような設定変更を行わずとも、Snowboyは動作できました。アナログピン接続のイヤフォンなどでは制限がでてくるのかもしれません。

おわりに

一応Snowboyが動作するようになりました。これからオリジナルHotwordを検知させていこうと思うのですが、 割と量が多くなったので、【後編】で続けていこうと思います。

たまに集中力が切れてしまって後編つづく詐欺をしてしまうこともあるのですがそこはご容赦ください。

/* -----codeの行番号----- */