HTMLとJavaScriptだけで音声入力

初めまして。
プログラマもすなるQiitaというものを（以下略）。


Google Chrome限定だけど、HTMLとJavaScriptだけで音声入力が実現できてしまうらしい。
というわけで、作ってみた。

この記事は、スマホとタブレット上でのGoogle Chrome にて、
音声入力して```<input type="text">```にテキスト入力するのが目的。
テキスト取得までをクラス（はJavaScriptでは無いけれど）化して、
以下のように簡単に呼び出すのが目的。
（※今回のは、自サイトで公開済みの内容と一部被ります）


```js:speech.html
speech_input = new SpeechInputWs( null );

// 録音開始ボタンの動作
$("#id_start_button").click(function(){
	var dfd = speech_input.start();
	dfd.done( function( text ){
		$("#id_output").val( text );
	});
});
```
AndroidスマホとタブレットのChrome 47で動作確認。


参考にさせていただいたのは、以下のサイト様。
http://qiita.com/inouet/items/2c9e218c05f547bb6852
http://www.cyokodog.net/blog/web-speechi-api/



SpeechInputWs() クラス（はJavaScriptでは無いけれど）の、
実装は下記。
（ところで、JavaScriptにおいて
　こういうクラスっぽいものを、なんと呼称すればよいのだ？）

SpeechInputWs()で、やってることは、
参考にしたサイト様（ http://qiita.com/inouet/items/2c9e218c05f547bb6852 ）の
コードをラッパーして、
音声取得中のオーバーレイdivで「音声入力中・・・」表示を行い、
完了したら候補を5個まで表示して、
選択されたradioボタンにてテキストを確定して、
jQueryのdeferred.done( function(text){} )の形式で返す、って内容。
「音声入力中・・・」の通知を出すdivも自前で描画。


```js:speech_input.js
/*
	[speech_input.js]
	encoding="UTF-8"
*/


// Googleの音声入力用のクラス定義
//   ref. http://qiita.com/inouet/items/2c9e218c05f547bb6852
//   ref. http://www.cyokodog.net/blog/web-speechi-api/
//   ref. https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html 【公式Spec】
// 
// noticeOverlay = null指定可能。その場合は、通知用divを自動生成。
// ,start() で、deferred が返るので、.done( text )で取得して任意に設定する。
//
// ※音声入力の実行可能不可能の自前判定は無いので、別途「スマホ／タブレットのみOK」などの
// 　処理を実装の事。
//
var SpeechInputWs = function( noticeOverlay ){
	var self = this;
	this.itsNoticeOverlayDiv = noticeOverlay;

	// 通知用のオーバーレイdivが無ければ自前で生成する。
	if( !this.itsNoticeOverlayDiv ){
		$("body").append( "<div id=\"_id_overlay_notice\">hoge</div>" );
		this.itsNoticeOverlayDiv = $("#_id_overlay_notice");
		this.itsNoticeOverlayDiv.css({
			"display" : "none",
			"width"   : "280px",
			"height"  : "160px",
			"text-align" : "left",
			"position"   : "fixed",
			"top"  : "24px",
			"left" : "24px",
			"z-index" : "100",
			"background-color": "#cccccc"
		});
	}

	// 一応、毎回初期化？？？
	window.SpeechRecognition = window.SpeechRecognition || webkitSpeechRecognition;

	// オブジェクト生成
	this.itsRecognition = new webkitSpeechRecognition();
	this.itsRecognition.lang = 'ja';
	this.itsRecognition.maxAlternatives = 3;

	// 終了時に呼び出されるメソッドを設定
	this.itsRecognition.addEventListener("result", function(event){
		// この場面でのthisはグローバル扱いの可能性があるので、クロージャーのselfを経由しておく。
		self._selectTextFromSpeechResult( event.results );
	});
	this.itsLastDfd = null;
};
SpeechInputWs.prototype._selectTextFromSpeechResult = function( speechResult ){
	// ※予め、this.itsLastDfd にDeferred::promise() が入っている事。
	var dfd_notice = this.itsLastDfd;
	var candidate = speechResult.item(0);
	var i = candidate.length;
	var str;

	if ( i == 0 ){ 
		dfd.resolve( candidate.item(0).transcript );
		return dfd.promise();
	}

	// 【FIXME】あれ？ .maxAlternatives=3 が効いてない？？？
	if( i > 5 ){ i = 5; }

	// 候補を表示してユーザー入力を待つ。
	str = ""; // 下記で作成済みのformへ追加するスタイル。
	while( 0 < i-- ){
		str += "<div><input type=\"radio\" name=\"candidate\" value=\"" + candidate.item(i).transcript + "\">";
		str += candidate.item(i).transcript;
		str += "</input></div>";
	}
	this.itsNoticeOverlayDiv.find("form").eq(0).append( str );

	this.itsNoticeOverlayDiv.find( "form div" ).each( function(){
		$(this).click( function(){
			dfd_notice.resolve( $(this).find("input").eq(0).val() );
		});
	});
	this.itsNoticeOverlayDiv.find( "form input[type='button']" ).each( function(){
		// 動作を変更する（stop()処理が不要になる。また投げる先のdfdが異なる）。
		$(this).click( function(){
			dfd_notice.reject( null );
		});
	});

	// 戻り値なし。
};
SpeechInputWs.prototype.start = function() {
	var self = this;
	var dfd_sound = new $.Deferred();
	var str = "　<form>音声入力中・・・ &nbsp; <input type=\"button\" value=\"中止\"></input><br>";
	str += "</form>";

	// 音声入力の受付を開始
	this.itsRecognition.start(); 

	// ユーザー通知系の処理
	this.itsNoticeOverlayDiv.empty();
	this.itsNoticeOverlayDiv.append( str );
	this.itsNoticeOverlayDiv.show();
	this.itsNoticeOverlayDiv.find("form input[type='button']").each( function(){
		$(this).click( function(){
			self.itsRecognition.stop();
			self.itsNoticeOverlayDiv.hide();
			dfd_sound.reject( null );
		});
	});

	// 録音終了時トリガーを受けるDeferredを設定
	this.itsLastDfd = new $.Deferred();
	this.itsLastDfd.done( function( text ){
		dfd_sound.resolve( text );
	}).fail( function(){
		dfd_sound.reject( null );
	}).always( function(){
		self.itsNoticeOverlayDiv.hide();
	});

	return dfd_sound.promise();
};
```

※なお、「スマホ／タブレットからのアクセスか？」で判定して音声入力ボタンを有効／無効で切り替えるとよいかも？ 当方の場合、アクセス判定には https://w3g.jp/blog/js_browser_sniffing2015 のコードを利用させてもらっている。サンクス！
※何も考えずに、```new new SpeechInputWs( null );```のところで、```try{}catch (e){}```するだけで、実行可能環境か否かの判定できた（2016.06.03追記）。何で最初は上手く判定できなかったんだろう？