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を使うことで,遠隔地のロボットとの双方向音声通話が可能になりました.
しかし,バッファリングのサイズやイベントハンドリングなどをチューニングしていないため,往復では数秒程度の遅延が出てしまいました.
今後は,より高いリアルタイム性を追求していきたいです.
