「コトバツナギ」というWebコンテンツを作成し、HTML5 Japan Cup 2014で最優秀賞を頂きました。コトバツナギは、SpeechRecognitionとWebRTCを軸にしたマルチユーザーコンテンツです。このコンテンツで使っている主な技術の解説をしたいと思います。
概要
コトバツナギは、声に出して声で遊ぶマルチユーザーコトバアソビゲームです。
PCサイトを開き、スマートフォンを皆で持って集まって遊ぶゲームになっています。スマートフォンの代わりにマイク機能付きのノートPCでも可です。
WebRTCとwebkitSpeechRecognitionを使っているため、2014年8月の時点ではPC・スマホともにChromeブラウザのみ対応です。iOSのChromeはWebRTCとwebkitSpeechRecognitionに非対応のため不可ですが、MacのChromeは動作します。一人でも遊べます。
動作環境が限られるので遊べないユーザーがいる場合もあると思い、デモ動画も用意しました。
解説文字無しの動画はこちら → [http://youtu.be/G6HGNISv4mo]
コトバツナギの主な技術をまとめると、
- SSL
- WebRTC(getUserMedia)
- WebRTC(PeerJS)
- SpeechRecognition
- SpeechSynthesis
- Web Audio
- テキスト解析
- 画像検索
で、できてます。
次章より詳しく説明していきます。
getUserMediaとSSL
WebRTCでカメラ・マイク入力を利用可能にするgetUserMediaは、使用する度にブラウザに許可を得るダイアログが出てしまいます。このままだとゲームとして遊ぶには問題がありますが、httpsでアクセスできるようにすると回避できるため、SSLを導入しました。httpsの場合、アクセスしているドメインに対して最初に1回だけ許可すると、その後の許可は不要になります。(ブラウザを閉じても、設定をクリアするまで有効)
コトバツナギでは、カメラ・マイクでそれぞれ許可ダイアログが出るので、最初に計2回の許可が必要になります。getUserMediaのvideo・audioを一度に扱えば一回で済みますが、マイク入力は使えるけどカメラが使えない(またはカメラの利用が嫌だ)という場合にも対応するため、このコンテンツではそれぞれの許可を分けました。
PeerJS
PCとスマートフォン間でWebRTCによる通信を実現するためにPeerJSを利用しています。PeerJSは無料で使えるPeerServer Cloud serviceを用意してくれていますがSSLに対応していません。今回は、レンタルしているVPSにPeerServerをインストールして、下記のサーバーサイドjavascriptをnode.jsのforeverコマンドで起動し、PeerServerを立てました。
var fs = require('fs'); var PeerServer = require('peer').PeerServer; var server = new PeerServer({ port: [ポート番号を指定します], path: '[パスを指定したい場合は記述します]', ssl: { key: fs.readFileSync('[sslのkeyファイルのパスが入ります]'), certificate: fs.readFileSync('[sslのcrtファイルのパスが入ります]') } });
アイコトバでハジメル
PCとスマートフォンを接続する際に、数字入力や文字入力をするコンテンツがよくありますが、その方法はあまり味気ないと以前より思っていたので、このコンテンツでは別の方法を検討しました。タップと声だけで遊べるコンテンツにしたかったので、アイコトバを声に出して言ってから始める仕組みにしています。
コトバツナギではアイコトバやゲーム本編での音声認識に、webkitSpeechRecognitionを使ってます。アイコトバは予めいくつか用意しておいて、認識した音声を対応する数字に変換するという仕組みをとっています。
var recognition = new webkitSpeechRecognition(); recognition.maxAlternatives=1; //webkitSpeechRecognitionは複数の解析候補を出力してくれる場合があるが、このコンテンツでは一つだけに絞って使用。 recognition.onresult = function(event) { //音声認識の解析結果のテキスト var resultText=event.results[0][0].transcript;//resultTextを予め用意してあるリストに照合して、マッチしたら該当の数字に変換
} recognition.start();
なお、webkitSpeechRecognitionは、onresult以外に、onstart、onend、onaudiostart、onsoundstart、onspeechstart、onspeechend、onsoundend、onaudioend、onnomatch、onerrorという多くのイベントがあり、必要に応じて使い分けることが重要になります。
解析結果(上記resultText)を数字に変換したら、WebRTCのDataChannel(PeerJSのDataConnection)を利用してPC側のゲーム本体と通信します。
var peer = new Peer('[自分のID番号]', {host: '[PeerServerのhost]', port: ['ポート番号'], path: '[パスを指定してPeerServerを立ち上げた場合は記述]'});var connect = peer.connect('[接続先のゲーム本体のID番号。このコンテンツでは、resultTextのテキストを数字に変換]'); connect.on('open', function(){ //接続成功 } connect.on('data', function(data){ //接続先から送られてきたdataを処理 }
数字入力や文字入力を使った方が動作は安定するのですが、あえてアイコトバを使うようにしたのは、声に出して遊ぶゲームであることをゲーム本編の前にユーザーに体験してもらい、確認・練習してもらうという意図があります。また、ゲーム本編の前にマイク入力の許可をさせておきたいという狙いもあります。
ゲームモードの選択
ゲームは「シリトリでアソブ」「レンソウでアソブ」の2つのモードで遊べるようにしています。
ゲームモードを選択する際、スマートフォンの場合にはタップ操作以外に端末を右か左に傾けることでも選べるようにしました。参加者みんなが体を傾けて選択している絵が作れたら面白いと思い、こういう機能をつけてます。
端末側の傾き検知は、以下のようになっています。 必要に応じて、傾き検知を開始・停止できるようにしておくと便利です。
var peer = new Peer([自身のID番号],[PeerServer情報]); var connect = peer.connect([ゲーム本体のID番号]);//傾き検知が必要になったらstartDeviceMotion()を呼んでaddEventListenerする var startDeviceMotion=function(){ window.addEventListener('devicemotion',onDevicemotion); };
//必要ない時に止められるようにしておく var stopDeviceMotion=function(){ window.removeEventListener('devicemotion',onDevicemotion); };
//端末が傾くと呼ばれる。実際にはaddEventListnerしていると常に呼ばれ続ける。 var onDevicemotion=function(e){ var gravity = e.accelerationIncludingGravity; var data={mode:"gravity",gx:gravity.x,gy:gravity.y}; connect.send(data); };
ユーザーの端末傾き情報をゲーム側で受け取って、ゲームモード決定するところは以下のようになっています。 このコンテンツのように一対多で接続する場合は、WebRTCのDataChannel(PeerJSのDataConnection)を接続毎に保持して、各端末からの通信を処理します。
var users=new Array(); var peer = new Peer([ゲーム本体のID番号],[PeerServer情報]);peer.on('connection', function(connect) { //一対多の接続の場合は端末から接続がある毎に呼ばれるので、connectを保持しておく。 //今回はUserクラスを用意してconnectを保持 var user=new User(connect); users.push(user) });
//各ユーザーがシリトリを選んでいるかレンソウを選んでいるか調べる var cnt=0; setInterval(function(){ var siritoriSelected=0; var rensouSelected=0;
for(var i=0;i<users.length;i++){ var selectmode=users[i].getSelectedMode(); if(selectmode=="siritori"){ siritoriSelected++; }else if(selectmode=="rensou"){ rensouSelected++; } } //一定カウント後にどちらが多いかによってモード決定 //(実際のゲームでは、全員の選択が落ち着くのを待ってからカウントしている) cnt++; if(cnt>3){ if(siritoriSelected>=rensouSelected){ //シリトリをはじめる }else{ //レンソウをはじめる } }
},500);
//Userクラス var User=function(connect){
connect.on('data', function(data){ //接続先の端末からいろいろな通信情報がここに送られるので、処理分岐できるようにしておく if(data.mode=="gravity"){ onGravity(data); } }); var onGravity=function(data){ //data.gx、data.gyを元にアイコンを移動させる。 }; //画面右の方にいるか左の方にいるか調べてシリトリかレンソウを返す this.getSelectedMode=function(){ var selectMode; if([左の方にいる場合]){ selectMode= "siritori"; } else{ selectMode= "rensou"; } return selectMode; }
}
ゲームモード決定時には「シリトリでアソブ」か「レンソウでアソブ」と、ゲーム本体から声が出ますが、これは音声合成で発話しています。
var speech=new SpeechSynthesisUtterance(); speech.text="しりとりであそぶ"; speech.lang = "ja-JP"; speech.volume = 1; speechSynthesis.speak(speech);//speechSynthesisはwindowが持っているオブジェクト
ゲーム本番
シリトリゲーム、または連想ゲームでコトバをつないで遊びます。
「シリトリでアソブ」のゲームの処理の流れを、利用している技術要素とともに図にしました。おおまかには、①~⑧の流れでコトバをつないでいきます。
カメラの利用が許可されている場合、回答する度に毎回操作端末側のカメラでユーザーを撮影して、ゲーム本体に画像を送信しています(フロー②)。
また、ユーザーの操作端末がノートPCの場合は、回答の音声を端末側で録音し、blobデータとしてゲーム本体に送信しています。(フロー②、③)録音にはrecorder.jsを利用しました。recorder.jsはrecorderWorker.jsがセットになっており、Web Workerとして使う仕組みになっているようです。
var mediaStream; var rec; var audioContext=new AudioContext();navigator.getUserMedia({audio:true}, function(stream) { mediaStream=stream; var source = audioContext.createMediaStreamSource(stream); rec=new Recorder(source,{workerPath:[recorderWorker.jsまでのパス]"}); rec.record(); }, function(err){ //エラーの場合は何もしない。 //スマホの場合はwebkitSpeechRecognitionと同時併用できないのでエラーになる。 } );
//録音終了したいタイミングで呼び出す var recStop=function(){ rec.stop(); mediaStream.stop();
//wav形式に変換したらonRecExportedを呼び出す rec.exportWAV(onRecExported);
};
//wav形式変換が完了するとblobデータが取得できる var onRecExported=function(blob){ //blobをWebRTCでゲーム本体に送信 };
スマートフォンの場合、2014年8月現在ではSpeechRecognitionとgetUserMediaのaudioを同時に使えない仕様になっていたので、残念ながらスマートフォンの場合は録音を諦めました。(SpeechRecognitionを同時に使わなければ、スマートフォンでも録音することができます)
なお、ここで録音撮影した音声・画像は、ゲーム終了時に使います。
ゲーム終了
ゲーム終了すると、これまでつないだコトバが連続して再生されます。
getUserMediaのvideoが使える場合、終了時に流れる各コトバに表示されているユーザー画像は、ゲーム中に回答した時に、毎回撮影したものを表示しています。(ゲーム本番のフロー②で撮影)それぞれのコトバを発声した時に、どんな顔をしていたかがわかるようになっています。(ページ上部のデモ動画参照)
また、ノートPCを使って回答したユーザーのコトバの場合、ゲーム中に回答した時に、録音した音声を再生します。(ゲーム本番のフロー①②で録音)上述の通りスマートフォンでは音声録音がSpeechRecognitionと併用できないので、スマートフォンを使って回答したユーザーのコトバの場合は、音声合成で代用します。
var audioContext=new AudioContext();//audioBlobはゲーム時に保存していたblobデータ if(audioBlob){ //audioContextはコンテンツ内でnewできる数に上限があるため、何度も同じ処理をする場合は、毎回newするのはなるべく避ける。 //このコンテンツのように同時発音する可能性がなければ、AudioContextは再利用した方がnew上限エラーにならないので良いと思われる。 if(!audioContext){ audioContext=new AudioContext(); } var source = audioContext.createBufferSource();
audioContext.decodeAudioData(audioBlob,function(buffer){ source.buffer = buffer; source.connect(audioContext.destination); source.start(0); });
//audioblobがない場合は合成音声で発話。recognitionTextはゲーム時に保存していた音声解析のテキスト。 }else{ var speech=new SpeechSynthesisUtterance(); speech.text=recognitionText; speech.lang = "ja-JP"; speech.volume = 1; speechSynthesis.speak(speech); }
最後に
コトバツナギの主な仕組みは上記のようになっています。ゲーム本編だけでなく、ゲーム終了後にも参加者みんなで結果を振り返って楽しんでもらえるように作っています。音声解析がなかなかうまくいかないことも多いですが、思わぬ解析結果になることも含めて楽しんでもらえれば幸いです。
よかったら遊んでみてください。