前回の記事では SensorTag で取得した値を AWS IoT に送信して、照度の値によって LED を点灯したり、SNSからメールを送信したりしてみましたが、今回はさらに Polly で照度の値を読み上げる音声ファイルを生成し、Raspberry Pi で再生する処理を追加してみたいと思います。
全体構成
全体の構成は下記のようになります。
前回と違うのは図の右下の赤線で囲んだ部分で、Lambda、Polly、s3との連携が追加になっています。
処理の流れとしては、 AWS IoT Rule で SNS によるメール送信を行なっていた部分の Action に Lambda の Function 呼び出しを追加し、その Function の中から Polly のAPIを呼び出して照度の値を読み上げる mp3 ファイルを生成して s3 に格納し、 presigned URL を生成して AWS IoT に Publish します。Raspberry Pi 側では新たに音声ファイルの presigned URL が Publish されるトピックにも Subscribe しておき、 presigned URL を受け取った場合はそこから mp3 をダウンロードして再生します。
Raspberry Pi での音声ファイルの再生
まずは下記ページを参考に Raspberry Pi 上で音声ファイルの再生を試してみます。
Raspberry Pi のイヤホンジャックにヘッドフォンを挿し、下記コマンドを実行してみると、すんなり再生されました。
pi@raspberrypi:~ $ aplay /usr/share/sounds/alsa/Front_Center.wav Playing WAVE '/usr/share/sounds/alsa/Front_Center.wav' : Signed 16 bit Little Endian, Rate 48000 Hz, Mono
下記のようなテスト用のコマンドも用意されているようです。
pi@raspberrypi:~ $ speaker-test -t sine -f 1000 speaker-test 1.0.28 Playback device is default Stream parameters are 48000Hz, S16_LE, 1 channels Sine wave rate is 1000.0000Hz Rate set to 48000Hz (requested 48000Hz) Buffer size range from 512 to 65536 Period size range from 512 to 65536 Using max buffer size 65536 Periods = 4 was set period_size = 16384 was set buffer_size = 65536 0 - Front Left Time per period = 1.409599 0 - Front Left Time per period = 2.563393
また、amixer というコマンドで現在のデバイスの設定の確認や変更ができます。
pi@raspberrypi:~ $ amixer info Card default 'ALSA'/'bcm2835 ALSA' Mixer name : 'Broadcom Mixer' Components : '' Controls : 6 Simple ctrls : 1 pi@raspberrypi:~ $ pi@raspberrypi:~ $ amixer scontrols Simple mixer control 'PCM',0 pi@raspberrypi:~ $ pi@raspberrypi:~ $ amixer scontents Simple mixer control 'PCM',0 Capabilities: pvolume pvolume-joined pswitch pswitch-joined Playback channels: Mono Limits: Playback -10239 - 400 Mono: Playback -2000 [77%] [-20.00dB] [on] pi@raspberrypi:~ $ pi@raspberrypi:~ $ amixer controls numid=3,iface=MIXER,name='PCM Playback Route' numid=2,iface=MIXER,name='PCM Playback Switch' numid=1,iface=MIXER,name='PCM Playback Volume' numid=5,iface=PCM,name='IEC958 Playback Con Mask' numid=4,iface=PCM,name='IEC958 Playback Default'
下記のようにハードウェアの情報を確認することもできます。
pi@raspberrypi:~ $ cat /proc/asound/modules 0 snd_bcm2835 pi@raspberrypi:~ $ pi@raspberrypi:~ $ cat /proc/asound/cards 0 [ALSA ]: bcm2835 - bcm2835 ALSA bcm2835 ALSA pi@raspberrypi:~ $
aplay は mp3 ファイルは再生できないので、 mpg321 を使ってみます。下記コマンドでインストールします。
pi@raspberrypi:~ $ sudo apt-get install mpg321
下記のように mp3 ファイルを指定することで再生することができます。
pi@raspberrypi:~/sounds $ mpg321 mondo_01.mp3
Polly と連携するための Lambda Function
まずは Lambda Function の全文を掲載しておきます。
import json import boto3 from boto3 import Session from boto3 import resource from contextlib import closing REGION = 'ap-northeast-1' POLLY_REGION = 'us-east-1' BUCKET_NAME = 'hiroaki.akanuma.iot' FILE_NAME_BASE = 'voices/%s_lux.mp3' SPEECH_BASE = '現在の照度は、%sルクスです。' AWS_IOT_TOPIC = '/iot/sensor_tag/voices' session = Session(region_name = POLLY_REGION) polly = session.client('polly') s3 = resource('s3') bucket = s3.Bucket(BUCKET_NAME) iot = boto3.client('iot-data') def synthesize_speech(lux): speech_text = SPEECH_BASE % lux response = polly.synthesize_speech( Text = speech_text, OutputFormat = 'mp3', VoiceId = 'Mizuki' ) return response['AudioStream'] def put_to_s3(audio_stream, file_name): with closing(audio_stream) as stream: bucket.put_object( Key = file_name, Body = stream.read(), ContentType = 'audio/mpeg' ) def generate_presigned_url(file_name): return boto3.client('s3').generate_presigned_url( ClientMethod = 'get_object', Params = {'Bucket' : BUCKET_NAME, 'Key' : file_name}, ExpiresIn = 3600, HttpMethod = 'GET' ) def publish_to_iot(speech_url): iot.publish( topic = AWS_IOT_TOPIC, qos = 0, payload = json.dumps({'speech_url': speech_url}) ) def lambda_handler(event, context): lux = event['lux'] audio_stream = synthesize_speech(lux) file_name = FILE_NAME_BASE % lux put_to_s3(audio_stream, file_name) presigned_url = generate_presigned_url(file_name) publish_to_iot(presigned_url) return presigned_url
event に照度の値が入ってくるのでまずはそれを取り出し、読み上げ用のテキストを作成したらそれを Polly の synthesize_speech メソッドに渡して、結果を AudioStream として取得します。出力フォーマットには mp3 を指定し、日本語での読み上げなので VoiceId には Mizuki
を指定しています。レスポンスの中にはメタデータ等も含んでいますが、今回は使用しなかったので、 AudioStream のみを取り出しています。ちなみに Polly はまだ東京リージョンでは使用できないので、 Polly のクライアント取得時のリージョンにバージニアを指定しています。
次に AudioStream から音声データを取得し、それを s3 に格納します。ファイル名は読み上げている照度の値を使って、それぞれの値ごとの音声ファイルを作成します。
そして Raspberry Pi から認証なしで s3 上の音声ファイルにアクセスできるように、 presigned URL を発行して、その URL を音声ファイルの URL のやりとり用の AWS IoT トピックに Publish しています。AWS IoT Things の Shadow 更新用とは別のトピックになります。
この Lambda Function を Rule の Action として追加します。
Raspberry Pi 側での読み上げ
Raspberry Pi 側では音声ファイルのURLのやりとり用のトピックに Subscribe して、音声ファイルを読み上げる処理を追加します。
SPEECH_TOPIC = '/iot/sensor_tag/voices' SOUNDS_DIR = '/home/pi/sounds/voices' def run_speech_thread(log) log.info("Running speech thread.") Thread.new do begin MQTT::Client.connect(host: BEAM_URL) do |client| client.subscribe(SPEECH_TOPIC) log.info("Subscribed to the topic: #{SPEECH_TOPIC}") client.get do |topic, json| speech_url = JSON.parse(json)['speech_url'] speech_uri = URI.parse(speech_url) speech_file = speech_uri.path.split('/').last speech_file_path = "#{SOUNDS_DIR}/#{speech_file}" unless File.exist?(speech_file_path) log.info("Opening URL: #{speech_url}") open(speech_url) do |file| open(speech_file_path, 'w+b') do |out| out.write(file.read) end end end log.info("Speaking: #{speech_file_path}") Open3.capture3("mpg321 #{speech_file_path}") end end rescue => e log.error(e.backtrace.join("\n")) end end end
MQTT でトピックに Subscribe して client.get
するとそこで待ち受けるようになるため、とりあえず今回は今までの Shadow 更新用の処理とは別スレッドで音声ファイル用トピックに Subscribe して処理を行うようにしました。トピックからメッセージを受信したら、音声ファイルのURLをパースして、同名のファイルがローカルにない場合は s3 からダウンロードします。そして mpg321
コマンドを実行して音声ファイルを再生しています。
動作確認
Lambda Function の保存や Rule での Action の追加を行ったら、Subscribeします。
$ sudo bundle exec ruby subscribe.rb
そして SensorTag のデータの Publish を開始します。
$ sudo bundle exec ruby publish.rb
すると、照度の値によって照明の ON/OFF が切り替わるタイミングで、音声ファイルも再生され、照度が読み上げられることになります。
まとめ
Polly を使うことで手軽に音声ファイルを作成することができ、生成されたファイルをローカルに保存して繰り返し使うこともできますので、上手く使えばリーズナブルに音声を使ったサービスを作ることができます。今回は AWS SDK の認証情報をデバイス上におきたくなかったので、 SORACOM Beam 経由の AWS IoT の Rule から使うやり方をとりましたが、 AWS SDK から直接 Polly を使えばもっと直接的に手軽に音声ファイルを利用できると思います。
今回のコードは下記リポジトリにも公開しています。
ちなみに今回は下記ページを参考にさせていただきました。