4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unity(C#)】Watson API × OculusQuestで音声認識

Last updated at Posted at 2021-04-04

はじめに

VRヘビーユーザーからすれば当たり前のことかもしれませんが、
OculusQuestにマイクが搭載されているのはご存じでしたでしょうか。

ボイスチャットに利用されることがほとんどで、
その他の用途で使われている事例をあまり見たことがありませんでした。
(たぶん世の中にはたくさんある)

しかし、最近目にした記事にボイスチャット以外の用途でマイクを使った事例がありました。
【参考リンク】:Synamon、ロゼッタと「リアルタイム多言語翻訳システム装備のVRオフィス」を共同開発

一言で説明すると、翻訳VRアプリです。
マイクを音声認識の受け口として利用しています。

そこで、私も勉強がてら"OculusQuestのマイクを利用したVRアプリ作りに挑戦してみたい"と思い、実際に作りました。

Watson API

先ほどのアプリで利用した音声認識の部分はWatson APIを利用しています。

利用にはアカウントの登録とリソースの作成が必要です。
こちらからログイン後、検索欄からSpeech To Textを選んでリソースを作成します。

Watson3.PNG

フリー版だと500分/月分が無料で30日で使用できなくなるようです。

リソースの作成を終えたら下記画面からAPIキーを取得します。

Watson4.PNG

##プロジェクトの下準備
続いてプロジェクトの下準備を行います。
まずは、プラットフォームをAndroidに変更しておきます。これは後からでも構いませんが私は最初にしました。

そして、Api Compatibility Level.NET 4.xに変更します。
これをしないと後述のSDKの導入でエラーを吐きます。

Watson1.PNG

続いて、公式のGitHubからSDKを取得します。
下記リンク先から2つZipファイルをダウンロードします。
watson-developer-cloud/unity-sdk
IBM/unity-sdk-core

Zipを解凍したらそれぞれ下記のように名前を変更し、プロジェクトのAssets配下に移動します。

Watson2.PNG

成功すれば、初回だけIBMのログイン画面に遷移するポップアップダイアログが出現します。
出現しなかったり、ダイアログを消してしまったりしても、
先ほど作成したリソースの画面でAPIキーを確認すればいいだけなので問題ないです。

音声認識ではマイクを使用するのでパーミッションの追記が必要です。
Oculus Integrationを導入後、Unityのツールバーに出現するOculus/Tools/Create strore -compatible AndroidManifest.xmlを実行し、AndroidManifestを作成します。
そしてマイクのパーミッションを追加します。categoryLAUNCHERが無いと怒られたのでそちらも追加しました。

<?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

##サンプルデモ

音が無いですが、ボタンを押しながらしゃべると音声認識が実行されるサンプルです。
SpeechSample.gif

##コード

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を導入する以前からPUN2Photon 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を作る

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?