はじめに
VRヘビーユーザーからすれば当たり前のことかもしれませんが、
OculusQuestにマイクが搭載されているのはご存じでしたでしょうか。
ボイスチャットに利用されることがほとんどで、
その他の用途で使われている事例をあまり見たことがありませんでした。
(たぶん世の中にはたくさんある)
しかし、最近目にした記事にボイスチャット以外の用途でマイクを使った事例がありました。
【参考リンク】:Synamon、ロゼッタと「リアルタイム多言語翻訳システム装備のVRオフィス」を共同開発
一言で説明すると、翻訳VRアプリです。
マイクを音声認識の受け口として利用しています。
そこで、私も勉強がてら"OculusQuestのマイクを利用したVRアプリ作りに挑戦してみたい"と思い、実際に作りました。
勉強がてら作成していた翻訳VRできました!😎
— KENTO⚽️XRエンジニア😎Zenn100記事マラソン挑戦中29/100 (@okprogramming) April 3, 2021
OculusQuestのマイクで拾った音声を音声認識APIに渡して、認識結果を翻訳APIに渡す、、、というやり方です💪
次はマルチ対応していきます😆#OculusQuest #Unity pic.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"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:installLocation="auto">
<application android:label="@string/app_name" android:icon="@mipmap/app_icon" android:allowBackup="false">
<activity android: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>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> //追記
<category android:name="android.intent.category.INFO" />
<category android:name="com.oculus.intent.category.VR" />
</intent-filter>
<meta-data android:name="com.oculus.vr.focusaware" android:value="true" />
</activity>
<meta-data android:name="unityplayer.SkipPermissionsDialog" android:value="false" />
<meta-data android:name="com.samsung.android.vr.application.mode" android:value="vr_only" />
<meta-data android:name="com.oculus.supportedDevices" android:value="quest|quest2" />
</application>
<uses-feature android:name="android.hardware.vr.headtracking" android:version="1" android:required="true" />
<uses-permission android: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
というサンプルシーン内のコードを改変しました。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using IBM.Watson.SpeechToText.V1;
using IBM.Cloud.SDK;
using IBM.Cloud.SDK.Authentication.Iam;
using IBM.Cloud.SDK.Utilities;
using IBM.Cloud.SDK.DataTypes;
public class CustomExampleStreaming : MonoBehaviour
{
[Space(10)]
[Tooltip("The service URL (optional). This defaults to \"https://api.us-south.speech-to-text.watson.cloud.ibm.com\"")]
[SerializeField]
private string _serviceUrl;
[Tooltip("Text field to display the results of streaming.")]
public Text ResultsField;
[Header("IAM Authentication")] [Tooltip("The IAM apikey.")] [SerializeField]
private string _iamApikey;
[Header("Parameters")]
[Tooltip("The Model to use. This defaults to en-US_BroadbandModel")]
[SerializeField]
private string _recognizeModel;
private int _recordingRoutine = 0;
private string _microphoneID = null;
private AudioClip _recording = null;
private int _recordingBufferSize = 1;
private int _recordingHZ = 22050;
private SpeechToTextService _service;
void Start()
{
LogSystem.InstallDefaultReactors();
Runnable.Run(CreateService());
}
private void Update()
{
if (OVRInput.GetDown(OVRInput.RawButton.A))
{
StartRecording();
}
if (OVRInput.GetUp(OVRInput.RawButton.A))
{
StopRecording();
}
}
private IEnumerator CreateService()
{
if (string.IsNullOrEmpty(_iamApikey))
{
throw new IBMException("Plesae provide IAM ApiKey for the service.");
}
IamAuthenticator authenticator = new IamAuthenticator(apikey: _iamApikey);
while (!authenticator.CanAuthenticate()) yield return null;
_service = new SpeechToTextService(authenticator);
if (!string.IsNullOrEmpty(_serviceUrl))
{
_service.SetServiceUrl(_serviceUrl);
}
_service.StreamMultipart = true;
Active = true;
}
private bool Active
{
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);
}
else if (!value && _service.IsListening)
{
_service.StopListening();
}
}
}
private void StartRecording()
{
if (_recordingRoutine == 0)
{
UnityObjectUtil.StartDestroyQueue();
_recordingRoutine = Runnable.Run(RecordingHandler());
}
}
private void StopRecording()
{
if (_recordingRoutine != 0)
{
Microphone.End(_microphoneID);
Runnable.Stop(_recordingRoutine);
_recordingRoutine = 0;
}
}
private void OnError(string error)
{
Active = false;
Debug.Log(error);
}
private IEnumerator RecordingHandler()
{
_recording = Microphone.Start(_microphoneID, true, _recordingBufferSize, _recordingHZ);
yield return null;
if (_recording == null)
{
StopRecording();
yield break;
}
var bFirstBlock = true;
var midPoint = _recording.samples / 2;
float[] samples = null;
while (_recordingRoutine != 0 && _recording != null)
{
int writePos = Microphone.GetPosition(_microphoneID);
if (writePos > _recording.samples || !Microphone.IsRecording(_microphoneID))
{
Debug.Log("Microphone disconnected.");
StopRecording();
yield break;
}
if ((bFirstBlock && writePos >= midPoint) || (!bFirstBlock && writePos < midPoint))
{
samples = new float[midPoint];
_recording.GetData(samples, bFirstBlock ? 0 : midPoint);
var record = new AudioData();
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
{
var remaining = bFirstBlock ? (midPoint - writePos) : (_recording.samples - writePos);
var timeRemaining = (float) remaining / (float) _recordingHZ;
yield return new WaitForSeconds(timeRemaining);
}
}
}
private void OnRecognize(SpeechRecognitionEvent result)
{
if (result != null && result.results.Length > 0)
{
foreach (var res in result.results)
{
foreach (var alt in res.alternatives)
{
ResultsField.text = alt.transcript;
}
}
}
}
private void OnRecognizeSpeaker(SpeakerRecognitionEvent result)
{
}
}
音声認識のコールバックに登録してあるOnRecognize
の内部で認識した音声をテキストに反映しています。
private void OnRecognize(SpeechRecognitionEvent result)
{
if (result != null && result.results.Length > 0)
{
foreach (var res in result.results)
{
foreach (var alt in res.alternatives)
{
ResultsField.text = alt.transcript;
}
}
}
}
下記リンクの手順でエディター上でも音声認識を行うことができます。
【参考リンク】:[【Unity】Oculus Link使ってEditor上でデバッグ]
(https://qiita.com/OKsaiyowa/items/0b1d505219c4b4bdc421)
しかし、まれに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を作る