1.はじめに
こんにちは.鈴本です.
前回の記事「Raspberry Pi Mouse / Catを中継サーバー経由で遠隔操作」の続きとして
双方向音声通話機能を実装しました.
構成は前回記事と同様,
ロボット
- Raspberry Pi Mouse / Cat
- Ubuntu Server 16.04.5 LTS (Xenial Xerus)
- Node.js v10.14.2
PC
- Microsoft Windows 10 Home 1803 (64bit)
- Google Chrome 72.0.3626.121 (Official Build) (64bit) or Firefox 65.0.2 (64 bit)
中継サーバー
- Raspberry Pi 3 Model B
- Ubuntu Server 16.04.5 LTS (Xenial Xerus)
- Node.js v10.14.2
通信プロトコル
- WebSocket
です.
また,これも前回同様ソースコードは
ソースコードを GitHub で公開しています.
どうやら,ChromeブラウザからPCのマイクへアクセスするには,
サイトがHTTPSであることが必須なようで,マイクの動作確認はFirefoxのみで行いました.
2.実装(ロボット→PC)
Node.jsの以下の2つのモジュールを利用しました.
ロボット側は
function InitMic() { mic_instance = mic({ rate: '44100', bitwidth: '16', encoding: 'signed-integer', device: 'plughw:1,0', channels: '1', debug: true, exitOnSilence: 6 }); var mic_input_stream = mic_instance.getAudioStream(); mic_input_stream.on('data', function(data) { socket.emit('r2s_MIC_RAW_DATA', {value : data}); }); } function StartMic() { mic_instance.start(); } function StpoMic() { mic_instance.stop(); }
なかんじで,PC側も
socket.on("s2p_MIC_RAW_DATA", function(data){ var arr = new Int16Array(data.value); var arrf = new Float32Array(arr.length); // 正規化 for (var i=0; i<arr.length; i++) { // 16bit音声なので!! arrf[i] = arr[i] / 32768.0; } mic_PlayAudioStream(arrf); }); var mic_ctx = new (window.AudioContext||window.webkitAudioContext); var mic_initial_delay_sec = 0; var mic_scheduled_time = 0; function mic_PlayChunk(audio_src, mic_scheduled_time) { if (audio_src.start) { audio_src.start(mic_scheduled_time); } else { audio_src.noteOn(mic_scheduled_time); } } function mic_PlayAudioStream(audio_f32) { var audio_buf = mic_ctx.createBuffer(1, audio_f32.length, 44100), audio_src = mic_ctx.createBufferSource(), current_time = mic_ctx.currentTime; audio_buf.getChannelData(0).set(audio_f32); audio_src.buffer = audio_buf; audio_src.connect(mic_ctx.destination); if (current_time < mic_scheduled_time) { mic_PlayChunk(audio_src, mic_scheduled_time); mic_scheduled_time += audio_buf.duration; } else { mic_PlayChunk(audio_src, current_time); mic_scheduled_time = current_time + audio_buf.duration + mic_initial_delay_sec; } }
なかんじ.
ロボット側ではマイク入力を, mic_input_stream
のイベントを監視して取得し,WebSocketに流し,
PC側ではそれを中継サーバー経由で受け取って,Web Audio APIであるAudioContextで再生しています.
PC側のコードで,音声が途切れ途切れにならないようになっている部分は,
WebAudio+WebSocketでブラウザへの音声リアルタイムストリーミングを実装する
を参考に実装しました.
3.実装(PC→ロボット)
ラズパイでのスピーカーでの再生には,Node.jsの
モジュールを使いました.
ラズパイにRaspbianをインストールしていると,ラズパイ上のオーディオジャックが簡単につかえるのですが,
代わりにUbuntuをインストールしている環境では, /boot/config.txt
を編集する必要があるようです.
(出典:How to enable sound on Raspberry Pi 3 running Ubuntu 16 – Raspberry Pi Stack Exchange)
PC側の実装は,
var spk_processor; function StartSpeaker(socket) { console.log("StartSpeaker"); navigator.mediaDevices.getUserMedia({ audio: true, video: false }).then( stream => { const context = new AudioContext(); const source = context.createMediaStreamSource( stream ); spk_processor = context.createScriptProcessor( 1024, 1, 1 ); source.connect( spk_processor ); spk_processor.connect( context.destination ); spk_processor.onaudioprocess = e => { // sint16に変換する var arrf = new Float32Array(e.inputBuffer.getChannelData(0)); var arr = new Int16Array(arrf.length); for (var i=0; i<arr.length; i++) { arr[i] = Math.round(arrf[i] * 32768.0); } console.log(arr); socket.emit("p2s_SPEAKER_RAW_DATA", {value : arr}); } } ) } function StopSpeaker() { spk_processor.disconnect(); spk_processor.onaudioprocess = null; }
で,ロボット側は,
socket.on('s2r_SPEAKER_RAW_DATA', function(data) { var len = Object.keys(data.value).length; var arr = new Int16Array(len); for (var i=0; i<len; i++) { arr[i] = data.value[i]; } StoreSpeakerData(arr); }); function StoreSpeakerData(data) { var buf = data.buffer; var arr8 = new Uint8Array(buf); speaker.write(arr8); }
となります.
Web Audio APIでブラウザからPCのマイクを操作し,音声を取得します.
onaudioprocess
イベントハンドラから引っ掛けて,データを送信しています.
受信側は,受信データを speaker
に渡しているだけですが,
speaker
は Stream
なので, Uint8Array
に変換しています.
( BufferArray
でもいけるだろ,って思っていたら普通にコケて,モジュールのコードを読むと, Uint8Array
しか受け付けていませんでした….)
4.まとめ
Node.jsのモジュールと,Web Browser標準搭載のWeb Audio APIを使うことで,遠隔地のロボットとの双方向音声通話が可能になりました.
しかし,バッファリングのサイズやイベントハンドリングなどをチューニングしていないため,往復では数秒程度の遅延が出てしまいました.
今後は,より高いリアルタイム性を追求していきたいです.