Raspberry Pi Mouse / Cat を遠隔操作(音声通話編)


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 に渡しているだけですが,
speakerStream なので, Uint8Array に変換しています.
BufferArray でもいけるだろ,って思っていたら普通にコケて,モジュールのコードを読むと, Uint8Array しか受け付けていませんでした….)

4.まとめ

Node.jsのモジュールと,Web Browser標準搭載のWeb Audio APIを使うことで,遠隔地のロボットとの双方向音声通話が可能になりました.
しかし,バッファリングのサイズやイベントハンドリングなどをチューニングしていないため,往復では数秒程度の遅延が出てしまいました.

今後は,より高いリアルタイム性を追求していきたいです.


Posted in Raspberry Pi Cat, Raspberry Pi Mouse, ブログ