はじめに
VRヘビーユーザーからすれば当たり前のことかもしれませんが、
OculusQuestにマイクが搭載されているのはご存じでしたでしょうか。
ボイスチャットに利用されることがほとんどで、
その他の用途で使われている事例をあまり見たことがありませんでした。
(たぶん世の中にはたくさんある)
しかし、最近目にした記事にボイスチャット以外の用途でマイクを使った事例がありました。
【参考リンク】:Synamon、ロゼッタと「リアルタイム多言語翻訳システム装備のVRオフィス」を共同開発
一言で説明すると、翻訳VRアプリです。
マイクを音声認識の受け口として利用しています。
そこで、私も勉強がてら"OculusQuestのマイクを利用したVRアプリ作りに挑戦してみたい"と思い、実際に作りました。
勉強がてら作成していた翻訳VRできました!😎
— KENTO⚽️XRエンジニア😎Zenn100記事マラソン挑戦中29/100 (@okprogramming) April 3, 2021
OculusQuestのマイクで拾った音声を音声認識APIに渡して、認識結果を翻訳APIに渡す、、、というやり方です💪
次はマルチ対応していきます😆#OculusQuest#Unitypic.twitter.com/k95D73gEnh
Watson API
先ほどのアプリで利用した音声認識の部分はWatson APIを利用しています。
利用にはアカウントの登録とリソースの作成が必要です。
こちらからログイン後、検索欄からSpeech To Textを選んでリソースを作成します。
フリー版だと500分/月分が無料で30日で使用できなくなるようです。
リソースの作成を終えたら下記画面からAPIキーを取得します。
プロジェクトの下準備
続いてプロジェクトの下準備を行います。
まずは、プラットフォームをAndroidに変更しておきます。これは後からでも構いませんが私は最初にしました。
そして、Api Compatibility Levelを.NET 4.xに変更します。
これをしないと後述のSDKの導入でエラーを吐きます。
続いて、公式のGitHubからSDKを取得します。
下記リンク先から2つZipファイルをダウンロードします。
watson-developer-cloud/unity-sdk
IBM/unity-sdk-core
Zipを解凍したらそれぞれ下記のように名前を変更し、プロジェクトのAssets配下に移動します。
成功すれば、初回だけIBMのログイン画面に遷移するポップアップダイアログが出現します。
出現しなかったり、ダイアログを消してしまったりしても、
先ほど作成したリソースの画面でAPIキーを確認すればいいだけなので問題ないです。
音声認識ではマイクを使用するのでパーミッションの追記が必要です。
Oculus Integrationを導入後、Unityのツールバーに出現するOculus/Tools/Create strore -compatible AndroidManifest.xmlを実行し、AndroidManifestを作成します。
そしてマイクのパーミッションを追加します。categoryにLAUNCHERが無いと怒られたのでそちらも追加しました。
<?xml version="1.0" encoding="utf-8" standalone="no"?><manifestxmlns:android="http://schemas.android.com/apk/res/android"android:installLocation="auto"><applicationandroid:label="@string/app_name"android:icon="@mipmap/app_icon"android:allowBackup="false"><activityandroid:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"android:configChanges="locale|fontScale|keyboard|keyboardHidden|mcc|mnc|navigation|orientation|screenLayout|screenSize|smallestScreenSize|touchscreen|uiMode"android:launchMode="singleTask"android:name="com.unity3d.player.UnityPlayerActivity"android:excludeFromRecents="true"><intent-filter><actionandroid:name="android.intent.action.MAIN"/><categoryandroid:name="android.intent.category.LAUNCHER"/> //追記
<categoryandroid:name="android.intent.category.INFO"/><categoryandroid:name="com.oculus.intent.category.VR"/></intent-filter><meta-dataandroid:name="com.oculus.vr.focusaware"android:value="true"/></activity><meta-dataandroid:name="unityplayer.SkipPermissionsDialog"android:value="false"/><meta-dataandroid:name="com.samsung.android.vr.application.mode"android:value="vr_only"/><meta-dataandroid:name="com.oculus.supportedDevices"android:value="quest|quest2"/></application><uses-featureandroid:name="android.hardware.vr.headtracking"android:version="1"android:required="true"/><uses-permissionandroid:name="android.permission.RECORD_AUDIO"/> //追記
</manifest>これでビルド環境が整いました。
バージョン
Unity 2019.4.8f1
Oculus Integration 25.0
watson-developer-cloud/unity-sdk v5.0.2
IBM/unity-sdk-core v1.2.2
サンプルデモ
音が無いですが、ボタンを押しながらしゃべると音声認識が実行されるサンプルです。
コード
SDK内のExampleStreamingというサンプルシーン内のコードを改変しました。
usingUnityEngine;usingSystem.Collections;usingUnityEngine.UI;usingIBM.Watson.SpeechToText.V1;usingIBM.Cloud.SDK;usingIBM.Cloud.SDK.Authentication.Iam;usingIBM.Cloud.SDK.Utilities;usingIBM.Cloud.SDK.DataTypes;publicclassCustomExampleStreaming:MonoBehaviour{[Space(10)][Tooltip("The service URL (optional). This defaults to \"https://api.us-south.speech-to-text.watson.cloud.ibm.com\"")][SerializeField]privatestring_serviceUrl;[Tooltip("Text field to display the results of streaming.")]publicTextResultsField;[Header("IAM Authentication")][Tooltip("The IAM apikey.")][SerializeField]privatestring_iamApikey;[Header("Parameters")][Tooltip("The Model to use. This defaults to en-US_BroadbandModel")][SerializeField]privatestring_recognizeModel;privateint_recordingRoutine=0;privatestring_microphoneID=null;privateAudioClip_recording=null;privateint_recordingBufferSize=1;privateint_recordingHZ=22050;privateSpeechToTextService_service;voidStart(){LogSystem.InstallDefaultReactors();Runnable.Run(CreateService());}privatevoidUpdate(){if(OVRInput.GetDown(OVRInput.RawButton.A)){StartRecording();}if(OVRInput.GetUp(OVRInput.RawButton.A)){StopRecording();}}privateIEnumeratorCreateService(){if(string.IsNullOrEmpty(_iamApikey)){thrownewIBMException("Plesae provide IAM ApiKey for the service.");}IamAuthenticatorauthenticator=newIamAuthenticator(apikey:_iamApikey);while(!authenticator.CanAuthenticate())yieldreturnnull;_service=newSpeechToTextService(authenticator);if(!string.IsNullOrEmpty(_serviceUrl)){_service.SetServiceUrl(_serviceUrl);}_service.StreamMultipart=true;Active=true;}privateboolActive{get=>_service.IsListening;set{if(value&&!_service.IsListening){_service.RecognizeModel=(string.IsNullOrEmpty(_recognizeModel)?"en-US_BroadbandModel":_recognizeModel);_service.DetectSilence=true;_service.EnableWordConfidence=true;_service.EnableTimestamps=true;_service.SilenceThreshold=0.01f;_service.MaxAlternatives=1;_service.EnableInterimResults=true;_service.OnError=OnError;_service.InactivityTimeout=-1;_service.ProfanityFilter=false;_service.SmartFormatting=true;_service.SpeakerLabels=false;_service.WordAlternativesThreshold=null;_service.EndOfPhraseSilenceTime=null;_service.StartListening(OnRecognize,OnRecognizeSpeaker);}elseif(!value&&_service.IsListening){_service.StopListening();}}}privatevoidStartRecording(){if(_recordingRoutine==0){UnityObjectUtil.StartDestroyQueue();_recordingRoutine=Runnable.Run(RecordingHandler());}}privatevoidStopRecording(){if(_recordingRoutine!=0){Microphone.End(_microphoneID);Runnable.Stop(_recordingRoutine);_recordingRoutine=0;}}privatevoidOnError(stringerror){Active=false;Debug.Log(error);}privateIEnumeratorRecordingHandler(){_recording=Microphone.Start(_microphoneID,true,_recordingBufferSize,_recordingHZ);yieldreturnnull;if(_recording==null){StopRecording();yieldbreak;}varbFirstBlock=true;varmidPoint=_recording.samples/2;float[]samples=null;while(_recordingRoutine!=0&&_recording!=null){intwritePos=Microphone.GetPosition(_microphoneID);if(writePos>_recording.samples||!Microphone.IsRecording(_microphoneID)){Debug.Log("Microphone disconnected.");StopRecording();yieldbreak;}if((bFirstBlock&&writePos>=midPoint)||(!bFirstBlock&&writePos<midPoint)){samples=newfloat[midPoint];_recording.GetData(samples,bFirstBlock?0:midPoint);varrecord=newAudioData();record.MaxLevel=Mathf.Max(Mathf.Abs(Mathf.Min(samples)),Mathf.Max(samples));record.Clip=AudioClip.Create("Recording",midPoint,_recording.channels,_recordingHZ,false);record.Clip.SetData(samples,0);_service.OnListen(record);bFirstBlock=!bFirstBlock;}else{varremaining=bFirstBlock?(midPoint-writePos):(_recording.samples-writePos);vartimeRemaining=(float)remaining/(float)_recordingHZ;yieldreturnnewWaitForSeconds(timeRemaining);}}}privatevoidOnRecognize(SpeechRecognitionEventresult){if(result!=null&&result.results.Length>0){foreach(varresinresult.results){foreach(varaltinres.alternatives){ResultsField.text=alt.transcript;}}}}privatevoidOnRecognizeSpeaker(SpeakerRecognitionEventresult){}}音声認識のコールバックに登録してあるOnRecognizeの内部で認識した音声をテキストに反映しています。
privatevoidOnRecognize(SpeechRecognitionEventresult){if(result!=null&&result.results.Length>0){foreach(varresinresult.results){foreach(varaltinres.alternatives){ResultsField.text=alt.transcript;}}}}下記リンクの手順でエディター上でも音声認識を行うことができます。
【参考リンク】:【Unity】Oculus Link使ってEditor上でデバッグ
しかし、まれにQuestのマイクが反応しなくなります。原因不明です。
その際はLinkの接続をいったん切る、Unityを再起動するなどすればだいたい直ります。
認識言語の設定はSpeechToTextService.RecognizeModelを変更することで切り替えることが可能です。
下記に一覧があります。
【参考リンク】:BM Cloud API Docs/Speech to Text
おわりに
翻訳VRは最終的に多人数翻訳コミュニケーションアプリに仕立てたいので、Watsonを導入する以前からPUN2とPhoton Voiceを導入していました。
しかし、ライブラリ間で同名のDllが存在する?とかなんとかでエラーを取り除くことができず、それに気づくまで動作しないことをWatsonのせいにしてました。(ごめんよWatson)
引き続き多人数対応をしていきたいので、どうすればライブラリ間の干渉によるエラーを取り除けるかも含めて調査します。
(Watson導入の前に、Androidのネイティブの音声認識機能がQuestでも動かないか検証して四苦八苦してました。結果動かずWatsonに助けてもらいました。その記録も供養の意を込めてそのうちメモしようと思っています。)
2021/04/05 追記
供養しました→【Unity(C#)】OculusQuestでAndroidネイティブの音声認識機能を呼び出せるのか検証
参考リンク
watson-developer-cloud/unity-sdk
UnityからIBM Watson APIを使う
UnityでWatsonと雑談APIを利用したChatBotを作る



