まくろぐ
更新: / 作成:

人間には目が 2 つあるがゆえに錯覚が生じることがあります。 ビリヤードなどの繊細な狙いが必要になるスポーツでは、この錯覚が意外と大きな障壁になるというお話です。 この話を聞いてからビリヤードをすると、目に入ってくる映像が毎回気になってしまうようになるでしょう。 フフフフフ…

ビリヤードのスタンスは、キューを顎の真下にもってくる方法と、左右どちらかの目の下にもってくる方法の 2 パターンに分かれると思いますが、今回は顎の真下にキューを置いた場合の話です。

下の図はかなりデフォルメして描いていますが、手球(白)で的球(青)を真っすぐ狙い、両目の焦点を的球に合わせている状態を示しています(目が怖いな)。

/p/hosqp2q/img-001.drawio.svg
図: 焦点を的球(青)に合わせている

このとき、的球は 1 つに見えるのですが、手前の手球やキューはなんと二重に見えているんです。 これを簡単に体験するには、数メートル先を見つめながら目の前に指を持ってきてください。 指が二重に見えるはずです。 人間の目ってそうなってるんですね。

もっと具体的に描くと、素振り中に遠くの的球を見つめているとき、手球やキューは次のように二重に見えます。 逆に、手球に焦点をあてているときは、遠くの的球が二重に見える(キューは少し二重に見える)という状態になります(図は省略)。

/p/hosqp2q/img-002.drawio.svg
図: 焦点が的球にあるときキューはこう見える

右目に入ってくる映像ではキューが左側にあるように見え、左目に入ってくる映像ではキューが右側にあるように見えます。 つまり、このように構えたときにキューを真っ直ぐ的球に向けるには、2 本に見えるキューのラインを 2 等分する中央のラインを的球に向けないといけないんです。 気持ち悪いですね。 でもこれが事実なんです。 もし、左右どちらかの映像だけを頼りにキュー方向を的球に向けようとすれば、手球は思わぬ方向に飛んでいくことでしょう。

このような錯覚を嫌う人は、右目あるいは左目の下にキューを置いて、片方の目に入ってくる映像を頼りに狙いを定めます。 理屈としては明らかにこちらの方が真っすぐ見えますが、視覚のアンバランスさを嫌う人は多いかもしれません。 スヌーカー選手は顎の下にキューを置くのがセオリーっぽいですね。

そういえば昔、ビリヤードの神様エフレン・レイズが「ストロークでは一箇所を見るのではなくて、キューのラインや手玉が走るライン全体を見なければいけない」と言っていたことを思い出しました。 これまでの話のように、全体を一度にハッキリと見ることはできないはずですが、神様には何か見えているのかもしれません。

関連記事

更新: / 作成:

何をするか?

Web ブラウザーの SpeechRecognition API を使って、音声認識をしてみます。 ここでは Svelte アプリケーションとして作成しますが、単純な HTML + JavaScript の組み合わせでもほぼ同様のコードになると思います(参考: Svelte 関連メモ)。

プロジェクトの準備

Svelte (SvelteKit) のプロジェクトがない場合は最初に作成しておきます。

プロジェクトの作成
$ npm create svelte@latest myapp
(選択肢が表示されたら TypeScript を選択しておく)
$ cd myapp
$ npm install

SpeechRecognition はブラウザ標準の API として策定されているものですが、TypeScript の型情報が認識されなかったので DefinitelyTyped で提供されている型情報 @types/dom-speech-recognition をインストールしておきます。

型情報のインストール
$ npm install --save-dev @types/dom-speech-recognition

これで、window.SpeechRecognition コンストラクタや、SpeechRecognitionResult などの各型情報を参照できるようになります。

SpeechRecognition の使い方

SpeechRecognition による音声認識の基本的な流れは次のようになります。

  1. SpeechRecognition インスタンスを生成して、各種パラメーターを設定する。
  2. SpeechRecognition.onresult プロパティに、認識結果を受け取るためのコールバック関数を設定する。
  3. SpeechRecognition.start() でマイクからの音声キャプチャと音声認識を開始する。

SpeechRecognition インスタンスの生成

まず、SpeechRecognition インスタンスを生成して、各種パラメータを設定していきます。 次のような感じで関数化しておくと分かりやすいです。

/**
 * SpeechRecognition インスタンスを生成し基本的なパラメーター設定を行います。
 */
function createSpeechRecognition(): SpeechRecognition {
	// ブラウザによって SpeechRecognition インスタンスの生成方法が異なる
	const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;

	const recognition = new SpeechRecognition();
	recognition.continuous = true; // 発声が途切れてもキャプチャを終了しない
	recognition.interimResults = true; // 認識途中の結果を得る
	recognition.lang = 'ja-JP'; // 認識対象の言語
	recognition.maxAlternatives = 1; // 認識結果の候補の最大数

	return recognition;
}

SpeechRecognition のコンストラクタは window オブジェクトから参照できますが、Chrome ブラウザの場合は window.webkitSpeechRecognition だったりするので注意してください。 ここで設定しているプロパティを簡単に説明しておきます。

recognition.continuous = true
発声が途切れても音声認識を継続するかどうかを制御します。 このプロパティを true にすると、ユーザーの発話が終了したと認識した後も継続して音声入力を受け付け、音声認識を続けます。 つまり、ずーっと継続して音声認識し続けたいアプリケーションなどでは true に設定します。 具体的には、start() 実行後に onresult プロパティにセットしたコールバック関数が呼び出され続けます。 デフォルトは false で、一区切りの発声が終わったときに音声キャプチャと認識が自動的に終了します。
recognition.interimResults = true
音声認識の結果に暫定の認識結果を含めるかを制御します。 このプロパティを true にすると、onresult コールバックに渡されるイベントオブジェクトの SpeechRecognitionResultList に、isFinal プロパティが false の結果(つまり暫定の認識結果)も含まれるようになります。 このあたりはちょっと難しいけど、この記事を最後まで読めばたぶん分かります。
recognition.lang
音声認識に使用する言語を設定するプロパティです。 デフォルトでは Web サイトに設定された言語属性が使用されます(例: ja-JP)。 実際のアプリケーションではユーザーが選択できるようにしておくと親切です。
recognition.maxAlternatives
音声認識の結果候補の最大数を設定あるいは取得するためのプロパティです。 デフォルトは 1 ですが、2 以上の値を設定することで、認識結果の候補を複数取得することができます。 多くのアプリケーションでは 1 のまま使うことになると思います。

result イベントで音声認識の結果を処理する

SpeechRecognition オブジェクトの onresult プロパティにコールバック関数をセットしておくと、音声認識処理が進むたびにその結果を受け取ることができます。 ここが一番難しいところです。

// let interimTranscript = ''; // 暫定の認識結果(表示用)
// let finalTranscript = ''; // 最終的な認識結果(表示用)

recognition.onresult = (event: SpeechRecognitionEvent) => {
	let interim = '';
	for (let i = event.resultIndex; i < event.results.length; i++) {
		const tr = event.results[i][0].transcript; // i 番目のチャンクの認識結果
		if (event.results[i].isFinal) {
			finalTranscript += tr; // 最終的な認識結果として表示
		} else {
			interim += tr;
		}
	}
	interimTranscript = interim; // 暫定の認識結果として表示
};

音声認識の結果は、SpeechRecognitionEvent 型のイベントオブジェクトとして渡されます。 音声認識は複数のチャンクが並行して処理されていきます。 各チャンクの認識結果(認識途中のこともある)が、イベントオブジェクトの results プロパティの中に SpeecRecognitionResult の配列として入っています。 i 番目のチャンクが実際にどのようなテキストとして認識されたかは、次のように参照することができます。

// i 番目の認識結果のテキスト
event.results[i][0].transcript;

入れ子の配列になっているのは、認識結果の候補が複数得られる可能性があるからです。 今回は、maxAlternatives=1 と設定したので、候補は 1 つしか含まれておらず、必ずインデックス 0 で参照します。

あとは、各チャンクの認識結果をループで取り出していけばよいのですが、各チャンクの認識はまだ途中のことがあります。 そのチャンクの認識が完了しているかどうかは、event.results[i].isFinal == true で判別することができます。

onresult の処理がややこしいのは、音声認識が少し進むたびにこのイベントハンドラーが呼び出されるからです。 つまり、認識が進むたびに、event.results の中の結果が更新されながら何度も呼び出されます。 onresult が呼び出されるたびに event.results を最初からループ処理するのは効率が悪いので、前回の onresult 呼び出しでどのチャンクまで認識完了したか(isFinal=true になったか)を示すインデックスを event.resultIndex で参照できるようになっています。 このインデックスをループの開始インデックスとすることで、更新されたチャンクだけを処理することができます。

/p/bf4cpjx/img-001.drawio.svg
図: SpeechRecognition.onresult に渡されるイベントオブジェクト

アプリケーション内で、音声認識が完了した部分のテキストを逐次処理したいときは、isFinal=true になった部分のチャンクを結合して扱えば OK です。 上記の実装例では、finalTranscript に格納されたテキストが、音声認識が完了した部分です。

音声認識を開始する

SpeechRecognition の設定が終わったら、あとは start() メソッドを呼び出すことで音声のキャプチャと認識が開始されます。 初回の実行時にはマイクを有効にしてよいかの確認ダイアログが表示されます。

音声認識を開始
recognition.start();

その他のメソッド

音声認識の開始・停止にかかわるメソッドとしては次のようなものがあります。

start()
音声のキャプチャと認識を開始します。

Starts the speech recognition service listening to incoming audio with intent to recognize grammars associated with the current SpeechRecognition.

stop()
音声のキャプチャと認識を停止します。そこまでに認識した結果は onresult として呼び出されます。

Starts the speech recognition service listening to incoming audio with intent to recognize grammars associated with the current SpeechRecognition.

abort()
音声のキャプチャと認識を中断します。そこまでに認識した結果は破棄され、onresult は呼び出されません。

Stops the speech recognition service from listening to incoming audio, and doesn’t attempt to return a SpeechRecognitionResult.

その他のイベントハンドラー

前述の実装例では onresult イベントハンドラーを使いましたが、他にも SpeechRecognition には次のようなイベントハンドラーが用意されています。 必要に応じて画面表示などの制御に使ってください。

onstart: () => void
音声認識が開始されたときに呼び出される関数を設定します。 Fired when the speech recognition service has begun listening to incoming audio with intent to recognize grammars associated with the current SpeechRecognition.
onend: () => void
音声認識が終了したときに呼び出される関数を設定します。 Fired when the speech recognition service has disconnected.
onaudiostart: () => void
音声キャプチャが開始されたときに呼び出される関数を設定します。 Fired when the user agent has started to capture audio.
onaudioend: () => void
音声キャプチャが終了したときに呼び出される関数を設定します。 Fired when the user agent has finished capturing audio.
onresult: (event: SpeechRecognitionEvent) => void
音声認識の結果があるときに呼び出される関数を設定します。 Fired when the speech recognition service returns a result — a word or phrase has been positively recognized and this has been communicated back to the app.
onerror: (event: SpeechRecognitionError) => void
音声認識が失敗したときに呼び出される関数を設定します。 Fired when a speech recognition error occurs.
onnomatch: () => void
はっきりとした発声を認識できずに終了した場合に呼び出される関数を設定します。 Fired when the speech recognition service returns a final result with no significant recognition. This may involve some degree of recognition, which doesn’t meet or exceed the confidence threshold.
onsoundstart: () => void
音声の入力を開始したときに呼び出される関数を設定します。 Fired when any sound — recognizable speech or not — has been detected.
onsoundend: () => void
音声の入力を終了したときに呼び出される関数を設定します。 Fired when any sound — recognizable speech or not — has stopped being detected.
onspeechstart: () => void
発声の開始を認識したときに呼び出される関数を設定します。 Fired when sound that is recognized by the speech recognition serviceas speech has been detected.
onspeechend: () => void
発声の終了を認識したときに呼び出される関数を設定します。 Fired when speech recognized by the speech recognition service has stopped being detected.

全体のコード

関連記事

更新: / 作成:

放置していた『世界樹の迷宮II』をやっとクリアしました。 前回クリアした『世界樹の迷宮I』から 3 年が経過してました😅

世界樹の迷宮は、ウィザードリィをリスペクトした 3D ダンジョンゲーの名作です。 今このゲームを手に入れようと思ったら、普通は Switch 版の 『世界樹の迷宮I・II・III HD REMASTER』 ですが、私がやったのは DS 版です(古っ)。

前作では地下に潜って行きましたが、今回は空飛ぶ城を目指して登って行きます。 世界樹に住みついている魔物を倒しながら進んでいくのですが、魔物たちは実はみんな◯◯だったという悲しいお話。

I のときもそうだったのですが、このシリーズ、最後のボス戦がほぼパズルゲームと化します。 ボスは特定のパターンで攻撃してくるのですが、うまくガードしないと、こっちの HP は 999 もないのに 10000 以上のダメージ をくらうという鬼畜仕様。 だから、うまく生き延びることができるパーティを揃えてから戦いに行かなきゃなのです。 ボス用のキャラクターを育て直すということもしばしば。 まぁそれが面白いという意見もあるんですけど、個人的にはドラクエみたいにお気に入りパーティでそのまま戦ってギリギリ倒す感じが好きだなぁ。

今回は王道的反則パターン?のカースメーカーという職業で挑みました。 この職業のペイントレードという技は、自分の HP が少なければ少ないほどダメージを与え、しかも相手の防御力を無視するという過激な技。 だから、ラスボス戦なのに、パーティが瀕死の状態で挑むというへんてこ図式 になります。 上記画像は、4 人のカースメーカーの HP がほとんど残っていませんが、そのままボス戦へ突入です。

そしてラスボスは瀕死パーティによるペイントレード連発で瞬殺。むごい・・・

III はまた 3 年後くらいにやるのかなぁ。

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ